├── .clj-kondo └── config.edn ├── .github └── workflows │ ├── clojure-linting.yaml │ ├── lein-test.yaml │ └── mend.yaml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CODEOWNERS ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── dev-resources ├── Makefile.i18n ├── bootstrapping │ ├── classpath │ │ └── bootstrap.cfg │ ├── cli │ │ ├── bootstrap.cfg │ │ ├── bootstrap_with_comments.cfg │ │ ├── duplicate_entries.cfg │ │ ├── duplicate_services │ │ │ ├── duplicates.cfg │ │ │ ├── split_one.cfg │ │ │ └── split_two.cfg │ │ ├── empty_bootstrap.cfg │ │ ├── fake_namespace_bootstrap.cfg │ │ ├── invalid_entry_bootstrap.cfg │ │ ├── invalid_service_graph_bootstrap.cfg │ │ ├── missing_definition_bootstrap.cfg │ │ ├── path with spaces │ │ │ └── bootstrap.cfg │ │ └── split_bootstraps │ │ │ ├── both │ │ │ ├── bootstrap_one.cfg │ │ │ └── bootstrap_two.cfg │ │ │ ├── empty │ │ │ ├── empty1.cfg │ │ │ └── empty2.cfg │ │ │ ├── one │ │ │ └── bootstrap_one.cfg │ │ │ ├── spaces │ │ │ ├── bootstrap with spaces one.cfg │ │ │ └── bootstrap with spaces two.cfg │ │ │ └── two │ │ │ └── bootstrap_two.cfg │ ├── cwd │ │ └── bootstrap.cfg │ ├── jar │ │ └── this-jar-contains-a-bootstrap-config-file.jar │ └── plugin │ │ └── bootstrap.cfg ├── config │ ├── conflictdir1 │ │ ├── config.conf │ │ └── config.ini │ ├── conflictdir2 │ │ ├── config.json │ │ └── config.properties │ ├── conflictdir3 │ │ ├── config.edn │ │ └── config.json │ ├── file │ │ ├── config.conf │ │ ├── config.edn │ │ ├── config.ini │ │ ├── config.json │ │ └── config.properties │ ├── inidir │ │ ├── bam.ini │ │ └── baz.ini │ └── mixeddir │ │ ├── bar.conf │ │ ├── baz.ini │ │ ├── foo.properties │ │ └── taco.json ├── logback.xml └── logging │ ├── logback-debug.xml │ ├── logback-evaluator-filter.xml │ └── logback-warn.xml ├── documentation ├── Bootstrapping.md ├── Built-in-Configuration-Service.md ├── Built-in-Services.md ├── Built-in-Shutdown-Service.md ├── Built-in-nREPL-Service.md ├── Command-Line-Arguments.md ├── Configuring-the-nREPL-Service.md ├── Defining-Services.md ├── Error-Handling.md ├── Helpful-Leiningen-Features.md ├── Index.md ├── Overview.md ├── Plugin-System.md ├── Polyglot-Support.md ├── Referencing-Services.md ├── Reloaded-Pattern.md ├── Restart-File.md ├── Service-Interfaces.md ├── Test-Utils.md ├── Trapperkeeper-Best-Practices.md └── Trapperkeeper-Quick-Start.md ├── examples ├── java_service │ ├── README.md │ ├── bootstrap.cfg │ ├── config.conf │ ├── logback.xml │ └── src │ │ ├── clj │ │ └── java_service_example │ │ │ └── java_service.clj │ │ └── java │ │ └── java_service_example │ │ └── ServiceImpl.java └── shutdown_app │ ├── README.md │ ├── bootstrap.cfg │ └── src │ └── examples │ └── shutdown_app │ └── test_external_shutdown.clj ├── ext ├── test │ ├── custom-exit-behavior │ ├── run-all │ ├── signal-handling │ └── top-level-cli └── travisci │ └── prep-macos ├── jenkins └── deploy.sh ├── locales ├── eo.po └── trapperkeeper.pot ├── plugin-test-resources ├── bad-plugins │ └── kitchensink-0.1.0.jar ├── plugins │ └── test-service.jar └── src │ └── test_services │ └── plugin_test_services.clj ├── project.clj ├── src └── puppetlabs │ └── trapperkeeper │ ├── app.clj │ ├── bootstrap.clj │ ├── common.clj │ ├── config.clj │ ├── core.clj │ ├── internal.clj │ ├── logging.clj │ ├── main.clj │ ├── plugins.clj │ ├── services.clj │ ├── services │ └── nrepl │ │ └── nrepl_service.clj │ └── services_internal.clj ├── test └── puppetlabs │ └── trapperkeeper │ ├── bootstrap_test.clj │ ├── config_test.clj │ ├── core_test.clj │ ├── custom_exit_behavior_test.clj │ ├── examples │ └── bootstrapping │ │ └── test_services.clj │ ├── internal_test.clj │ ├── logging_test.clj │ ├── optional_deps_test.clj │ ├── plugins_test.clj │ ├── services │ ├── config │ │ └── typesafe_test.clj │ └── nrepl │ │ ├── nrepl_service_test.clj │ │ └── nrepl_test_send_middleware.clj │ ├── services_internal_test.clj │ ├── services_namespaces_test │ ├── ns1.clj │ └── ns2.clj │ ├── services_test.clj │ ├── shutdown_test.clj │ ├── signal_handling_test.clj │ └── testutils │ ├── bootstrap.clj │ ├── logging.clj │ └── logging_test.clj └── tk /.clj-kondo/config.edn: -------------------------------------------------------------------------------- 1 | {:linters {:unresolved-symbol {:level :warning :exclude [(puppetlabs.trapperkeeper.services/service) 2 | (puppetlabs.trapperkeeper.core/defservice) 3 | (puppetlabs.trapperkeeper.core/service) 4 | (puppetlabs.trapperkeeper.services/defservice) 5 | (clojure.test/is [thrown+? thrown+-with-msg? logged?]) 6 | (puppetlabs.trapperkeeper.testutils.bootstrap/with-app-with-cli-data) 7 | (puppetlabs.trapperkeeper.testutils.bootstrap/with-app-with-config) 8 | (puppetlabs.trapperkeeper.testutils.bootstrap/with-app-with-empty-config) 9 | (puppetlabs.trapperkeeper.testutils.bootstrap/with-app-with-cli-args) 10 | (puppetlabs.trapperkeeper.testutils.logging/with-started) 11 | (puppetlabs.trapperkeeper.testutils.logging/with-logger-event-maps) 12 | (puppetlabs.trapperkeeper.testutils.logging/with-logged-event-maps)]} 13 | :invalid-arity {:skip-args [puppetlabs.trapperkeeper.services/service 14 | puppetlabs.trapperkeeper.services/defservice 15 | puppetlabs.trapperkeeper.core/defservice 16 | puppetlabs.trapperkeeper.core/service]} 17 | :refer-all {:level :off} 18 | :inline-def {:level :off} 19 | :deprecated-var {:level :off}} 20 | 21 | :lint-as {puppetlabs.trapperkeeper.core/defservice clojure.core/def 22 | puppetlabs.trapperkeeper.services/defservice clojure.core/def 23 | slingshot.slingshot/try+ clojure.core/try 24 | puppetlabs.kitchensink.core/while-let clojure.core/let}} -------------------------------------------------------------------------------- /.github/workflows/clojure-linting.yaml: -------------------------------------------------------------------------------- 1 | name: Clojure Linting 2 | 3 | on: 4 | pull_request: 5 | types: [opened, reopened, edited, synchronize] 6 | paths: ['src/**','test/**','.clj-kondo/config.edn','project.clj','.github/**'] 7 | 8 | jobs: 9 | clojure-linting: 10 | name: Clojure Linting 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: setup java 14 | uses: actions/setup-java@v4 15 | with: 16 | distribution: temurin 17 | java-version: 17 18 | - name: checkout repo 19 | uses: actions/checkout@v4 20 | - name: install clj-kondo (this is quite fast) 21 | run: | 22 | curl -sLO https://raw.githubusercontent.com/clj-kondo/clj-kondo/master/script/install-clj-kondo 23 | chmod +x install-clj-kondo 24 | ./install-clj-kondo --dir . 25 | - name: kondo lint 26 | run: ./clj-kondo --lint src test 27 | - name: eastwood lint 28 | run: | 29 | java -version 30 | lein eastwood -------------------------------------------------------------------------------- /.github/workflows/lein-test.yaml: -------------------------------------------------------------------------------- 1 | name: PR Testing 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | types: [opened, reopened, edited, synchronize] 7 | paths: ['src/**','test/**','project.clj'] 8 | 9 | jobs: 10 | pr-testing: 11 | name: PR Testing 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | version: ['8', '11', '17'] 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: checkout repo 19 | uses: actions/checkout@v3 20 | with: 21 | submodules: recursive 22 | - name: setup java 23 | uses: actions/setup-java@v3 24 | with: 25 | distribution: 'temurin' 26 | java-version: ${{ matrix.version }} 27 | - name: clojure tests 28 | run: lein test 29 | timeout-minutes: 30 -------------------------------------------------------------------------------- /.github/workflows/mend.yaml: -------------------------------------------------------------------------------- 1 | name: mend_scan 2 | on: 3 | schedule: 4 | # run every day at 4:00am 5 | - cron: 0 4 * * * 6 | workflow_dispatch: 7 | push: 8 | branches: 9 | - main 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: connect_twingate 15 | uses: twingate/github-action@v1 16 | with: 17 | service-key: ${{ secrets.TWINGATE_PUBLIC_REPO_KEY }} 18 | - name: checkout repo content 19 | uses: actions/checkout@v4 # checkout the repository content to github runner. 20 | with: 21 | fetch-depth: 1 22 | # install java which is required for mend and clojure 23 | - name: setup java 24 | uses: actions/setup-java@v4 25 | with: 26 | distribution: temurin 27 | java-version: 17 28 | # install clojure tools 29 | - name: Install Clojure tools 30 | uses: DeLaGuardo/setup-clojure@12.5 31 | with: 32 | # Install just one or all simultaneously 33 | # The value must indicate a particular version of the tool, or use 'latest' 34 | # to always provision the latest version 35 | cli: latest # Clojure CLI based on tools.deps 36 | lein: latest # Leiningen 37 | boot: latest # Boot.clj 38 | bb: latest # Babashka 39 | clj-kondo: latest # Clj-kondo 40 | cljstyle: latest # cljstyle 41 | zprint: latest # zprint 42 | # run lein gen 43 | - name: create pom.xml 44 | run: lein pom 45 | # download mend 46 | - name: download_mend 47 | run: curl -o wss-unified-agent.jar https://unified-agent.s3.amazonaws.com/wss-unified-agent.jar 48 | - name: run mend 49 | run: env WS_INCLUDES=pom.xml java -jar wss-unified-agent.jar 50 | env: 51 | WS_APIKEY: ${{ secrets.MEND_API_KEY }} 52 | WS_WSS_URL: https://saas-eu.whitesourcesoftware.com/agent 53 | WS_USERKEY: ${{ secrets.MEND_TOKEN }} 54 | WS_PRODUCTNAME: Puppet Enterprise 55 | WS_PROJECTNAME: ${{ github.event.repository.name }} 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Emacs 3 | *# 4 | *~ 5 | .#* 6 | 7 | .lein-failures 8 | .lein-repl-history 9 | target/ 10 | checkouts/ 11 | pom.xml 12 | .nrepl-port 13 | /resources/locales.clj 14 | /resources/puppetlabs/trapperkeeper/*.class 15 | /dev-resources/i18n/bin 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | lein: 2.9.1 3 | 4 | jobs: 5 | include: 6 | - jdk: openjdk8 7 | name: lein test (openjdk8) 8 | - jdk: openjdk11 9 | name: lein test (openjdk11) 10 | - jdk: openjdk8 11 | name: external tests (openjdk8) 12 | script: lein uberjar && ext/test/run-all 13 | - jdk: openjdk11 14 | name: external tests (openjdk11) 15 | script: lein uberjar && ext/test/run-all 16 | - name: external tests (openjdk11) 17 | # Apparently travis' lein support is broken right now 18 | language: java 19 | os: osx 20 | osx_image: xcode10.3 21 | script: | 22 | # prep in standalone script so things like set -x don't affect travis 23 | ext/travisci/prep-macos \ 24 | && export PATH="$(pwd)/ext/travisci/bin:$PATH" \ 25 | && lein uberjar \ 26 | && ext/test/run-all 27 | 28 | notifications: 29 | email: false 30 | 31 | cache: 32 | directories: 33 | - $HOME/.m2 34 | - $HOME/Library/Caches/Homebrew 35 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @puppetlabs/dumpling 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | Third-party patches are essential for keeping Puppet Labs open-source projects 4 | great. We want to keep it as easy as possible to contribute changes that 5 | allow you to get the most out of our projects. There are a few guidelines 6 | that we need contributors to follow so that we can have a chance of keeping on 7 | top of things. For more info, see our canonical guide to contributing: 8 | 9 | [https://github.com/puppetlabs/puppet/blob/master/CONTRIBUTING.md](https://github.com/puppetlabs/puppet/blob/master/CONTRIBUTING.md) 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include dev-resources/Makefile.i18n -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Trapperkeeper logo 3 | 4 | # Trapperkeeper 5 | 6 | [![Build Status](https://travis-ci.org/puppetlabs/trapperkeeper.png?branch=master)](https://travis-ci.org/puppetlabs/trapperkeeper) 7 | 8 | Trapperkeeper is a Clojure framework for hosting long-running applications and services. 9 | You can think of it as a sort of "binder" for Ring applications and other modular bits of Clojure code. 10 | 11 | ## Installation 12 | 13 | Add the following dependency to your `project.clj` file: 14 | 15 | [![Clojars Project](http://clojars.org/puppetlabs/trapperkeeper/latest-version.svg)](http://clojars.org/puppetlabs/trapperkeeper) 16 | 17 | ## Community 18 | 19 | * 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. 20 | * freenode: #trapperkeeper 21 | * [![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) 22 | 23 | 24 | 25 | ## Documentation 26 | 27 | You can find a quick-start, example code, and lots and lots of documentation in our: 28 | 29 | * [Documentation](documentation/Index.md) 30 | 31 | ## Lein Template 32 | 33 | A Leiningen template is available that shows a suggested project structure: 34 | 35 | lein new trapperkeeper my.namespace/myproject 36 | 37 | Once you've created a project from the template, you can run it via the lein alias: 38 | 39 | lein tk 40 | 41 | Note that the template is not intended to suggest a specific namespace organization; 42 | it's just intended to show you how to write a service, a web service, and tests 43 | for each. 44 | 45 | ## Related Projects 46 | 47 | Here are some additional projects that provide Trapperkeeper services, and 48 | other related functionality: 49 | 50 | * [trapperkeeper-webserver-jetty9](https://github.com/puppetlabs/trapperkeeper-webserver-jetty9): a Jetty9-based webserver for use with TK applications 51 | * [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 52 | * [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. 53 | * [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. 54 | * [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. 55 | * [trapperkeeper-scheduler](https://github.com/puppetlabs/trapperkeeper-scheduler): a TK service that provides an API for scheduling periodic background tasks 56 | 57 | ## License 58 | 59 | Copyright © 2013 Puppet Labs 60 | 61 | Distributed under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html) 62 | 63 | ## Support 64 | 65 | Please log tickets and issues at our [JIRA tracker](https://tickets.puppetlabs.com/browse/TK). 66 | 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). 67 | -------------------------------------------------------------------------------- /dev-resources/Makefile.i18n: -------------------------------------------------------------------------------- 1 | # -*- Makefile -*- 2 | # This file was generated by the i18n leiningen plugin 3 | # Do not edit this file; it will be overwritten the next time you run 4 | # lein i18n init 5 | # 6 | 7 | # The name of the package into which the translations bundle will be placed 8 | BUNDLE=puppetlabs.trapperkeeper 9 | 10 | # The name of the POT file into which the gettext code strings (msgid) will be placed 11 | POT_NAME=trapperkeeper.pot 12 | 13 | # The list of names of packages covered by the translation bundle; 14 | # by default it contains a single package - the same where the translations 15 | # bundle itself is placed - but this can be overridden - preferably in 16 | # the top level Makefile 17 | PACKAGES?=$(BUNDLE) 18 | LOCALES=$(basename $(notdir $(wildcard locales/*.po))) 19 | BUNDLE_DIR=$(subst .,/,$(BUNDLE)) 20 | BUNDLE_FILES=$(patsubst %,resources/$(BUNDLE_DIR)/Messages_%.class,$(LOCALES)) 21 | FIND_SOURCES=find src -name \*.clj 22 | # xgettext before 0.19 does not understand --add-location=file. Even CentOS 23 | # 7 ships with an older gettext. We will therefore generate full location 24 | # info on those systems, and only file names where xgettext supports it 25 | LOC_OPT=$(shell xgettext --add-location=file -f - /dev/null 2>&1 && echo --add-location=file || echo --add-location) 26 | 27 | LOCALES_CLJ=resources/locales.clj 28 | define LOCALES_CLJ_CONTENTS 29 | { 30 | :locales #{$(patsubst %,"%",$(LOCALES))} 31 | :packages [$(patsubst %,"%",$(PACKAGES))] 32 | :bundle $(patsubst %,"%",$(BUNDLE).Messages) 33 | } 34 | endef 35 | export LOCALES_CLJ_CONTENTS 36 | 37 | 38 | i18n: msgfmt 39 | 40 | # Update locales/.pot 41 | update-pot: locales/$(POT_NAME) 42 | 43 | locales/$(POT_NAME): $(shell $(FIND_SOURCES)) | locales 44 | @tmp=$$(mktemp $@.tmp.XXXX); \ 45 | $(FIND_SOURCES) \ 46 | | xgettext --from-code=UTF-8 --language=lisp \ 47 | --copyright-holder='Puppet ' \ 48 | --package-name="$(BUNDLE)" \ 49 | --package-version="$(BUNDLE_VERSION)" \ 50 | --msgid-bugs-address="docs@puppet.com" \ 51 | -k \ 52 | -kmark:1 -ki18n/mark:1 \ 53 | -ktrs:1 -ki18n/trs:1 \ 54 | -ktru:1 -ki18n/tru:1 \ 55 | -ktrun:1,2 -ki18n/trun:1,2 \ 56 | -ktrsn:1,2 -ki18n/trsn:1,2 \ 57 | $(LOC_OPT) \ 58 | --add-comments --sort-by-file \ 59 | -o $$tmp -f -; \ 60 | sed -i.bak -e 's/charset=CHARSET/charset=UTF-8/' $$tmp; \ 61 | sed -i.bak -e 's/POT-Creation-Date: [^\\]*/POT-Creation-Date: /' $$tmp; \ 62 | rm -f $$tmp.bak; \ 63 | if ! diff -q -I POT-Creation-Date $$tmp $@ >/dev/null 2>&1; then \ 64 | mv $$tmp $@; \ 65 | else \ 66 | rm $$tmp; touch $@; \ 67 | fi 68 | 69 | # Run msgfmt over all .po files to generate Java resource bundles 70 | # and create the locales.clj file 71 | msgfmt: $(BUNDLE_FILES) $(LOCALES_CLJ) clean-orphaned-bundles 72 | 73 | # Force rebuild of locales.clj if its contents is not the the desired one. The 74 | # shell echo is used to add a trailing newline to match the one from `cat` 75 | ifneq ($(shell cat $(LOCALES_CLJ) 2> /dev/null),$(shell echo '$(LOCALES_CLJ_CONTENTS)')) 76 | .PHONY: $(LOCALES_CLJ) 77 | endif 78 | $(LOCALES_CLJ): | resources 79 | @echo "Writing $@" 80 | @echo "$$LOCALES_CLJ_CONTENTS" > $@ 81 | 82 | # Remove every resource bundle that wasn't generated from a PO file. 83 | # We do this because we used to generate the english bundle directly from the POT. 84 | .PHONY: clean-orphaned-bundles 85 | clean-orphaned-bundles: 86 | @for bundle in resources/$(BUNDLE_DIR)/Messages_*.class; do \ 87 | locale=$$(basename "$$bundle" | sed -E -e 's/\$$?1?\.class$$/_class/' | cut -d '_' -f 2;); \ 88 | if [ ! -f "locales/$$locale.po" ]; then \ 89 | rm "$$bundle"; \ 90 | fi \ 91 | done 92 | 93 | resources/$(BUNDLE_DIR)/Messages_%.class: locales/%.po | resources 94 | msgfmt --java2 -d resources -r $(BUNDLE).Messages -l $(*F) $< 95 | 96 | # Use this to initialize translations. Updating the PO files is done 97 | # automatically through a CI job that utilizes the scripts in the project's 98 | # `bin` file, which themselves come from the `clj-i18n` project. 99 | locales/%.po: | locales 100 | @if [ ! -f $@ ]; then \ 101 | touch $@ && msginit --no-translator -l $(*F) -o $@ -i locales/$(POT_NAME); \ 102 | fi 103 | 104 | resources locales: 105 | @mkdir $@ 106 | 107 | help: 108 | $(info $(HELP)) 109 | @echo 110 | 111 | .PHONY: help 112 | 113 | define HELP 114 | This Makefile assists in handling i18n related tasks during development. Files 115 | that need to be checked into source control are put into the locales/ directory. 116 | They are 117 | 118 | locales/$(POT_NAME) - the POT file generated by 'make update-pot' 119 | locales/$$LANG.po - the translations for $$LANG 120 | 121 | Only the $$LANG.po files should be edited manually; this is usually done by 122 | translators. 123 | 124 | You can use the following targets: 125 | 126 | i18n: refresh all the files in locales/ and recompile resources 127 | update-pot: extract strings and update locales/$(POT_NAME) 128 | locales/LANG.po: create translations for LANG 129 | msgfmt: compile the translations into Java classes; this step is 130 | needed to make translations available to the Clojure code 131 | and produces Java class files in resources/ 132 | endef 133 | # @todo lutter 2015-04-20: for projects that use libraries with their own 134 | # translation, we need to combine all their translations into one big po 135 | # file and then run msgfmt over that so that we only have to deal with one 136 | # resource bundle 137 | -------------------------------------------------------------------------------- /dev-resources/bootstrapping/classpath/bootstrap.cfg: -------------------------------------------------------------------------------- 1 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/classpath-test-service 2 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/hello-world-service 3 | -------------------------------------------------------------------------------- /dev-resources/bootstrapping/cli/bootstrap.cfg: -------------------------------------------------------------------------------- 1 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/cli-test-service 2 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/hello-world-service 3 | -------------------------------------------------------------------------------- /dev-resources/bootstrapping/cli/bootstrap_with_comments.cfg: -------------------------------------------------------------------------------- 1 | # commented out line 2 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/hello-world-service # comment 3 | ; another commented out line 4 | ;puppetlabs.trapperkeeper.examples.bootstrapping.test-services/foo-test-service 5 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/foo-test-service ; comment 6 | -------------------------------------------------------------------------------- /dev-resources/bootstrapping/cli/duplicate_entries.cfg: -------------------------------------------------------------------------------- 1 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/hello-world-service 2 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/hello-world-service 3 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/hello-world-service 4 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/hello-world-service 5 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/hello-world-service 6 | -------------------------------------------------------------------------------- /dev-resources/bootstrapping/cli/duplicate_services/duplicates.cfg: -------------------------------------------------------------------------------- 1 | # cli and foo implement the same service protocol 2 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/cli-test-service 3 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/foo-test-service 4 | # test-service-two and test-service-two-duplicate implement the same service protocol 5 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/test-service-two 6 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/test-service-two-duplicate 7 | -------------------------------------------------------------------------------- /dev-resources/bootstrapping/cli/duplicate_services/split_one.cfg: -------------------------------------------------------------------------------- 1 | # cli and foo implement the same service protocol 2 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/foo-test-service 3 | # test-service-two and test-service-two-duplicate implement the same service protocol 4 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/test-service-two-duplicate 5 | -------------------------------------------------------------------------------- /dev-resources/bootstrapping/cli/duplicate_services/split_two.cfg: -------------------------------------------------------------------------------- 1 | # cli and foo implement the same service protocol 2 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/cli-test-service 3 | # test-service-two and test-service-two-duplicate implement the same service protocol 4 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/test-service-two 5 | -------------------------------------------------------------------------------- /dev-resources/bootstrapping/cli/empty_bootstrap.cfg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puppetlabs/trapperkeeper/f7772765f17d3a0bc0d8af022d811eaa6cc0c539/dev-resources/bootstrapping/cli/empty_bootstrap.cfg -------------------------------------------------------------------------------- /dev-resources/bootstrapping/cli/fake_namespace_bootstrap.cfg: -------------------------------------------------------------------------------- 1 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/cli-test-service 2 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/hello-world-service 3 | non-existent-service/test-service 4 | -------------------------------------------------------------------------------- /dev-resources/bootstrapping/cli/invalid_entry_bootstrap.cfg: -------------------------------------------------------------------------------- 1 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/foo-test-service 2 | This is not a legit line. 3 | -------------------------------------------------------------------------------- /dev-resources/bootstrapping/cli/invalid_service_graph_bootstrap.cfg: -------------------------------------------------------------------------------- 1 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/invalid-service-graph-service 2 | -------------------------------------------------------------------------------- /dev-resources/bootstrapping/cli/missing_definition_bootstrap.cfg: -------------------------------------------------------------------------------- 1 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/cli-test-service 2 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/hello-world-service 3 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/non-existent-service 4 | -------------------------------------------------------------------------------- /dev-resources/bootstrapping/cli/path with spaces/bootstrap.cfg: -------------------------------------------------------------------------------- 1 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/cli-test-service 2 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/hello-world-service 3 | -------------------------------------------------------------------------------- /dev-resources/bootstrapping/cli/split_bootstraps/both/bootstrap_one.cfg: -------------------------------------------------------------------------------- 1 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/cli-test-service 2 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/hello-world-service 3 | -------------------------------------------------------------------------------- /dev-resources/bootstrapping/cli/split_bootstraps/both/bootstrap_two.cfg: -------------------------------------------------------------------------------- 1 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/test-service-two 2 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/test-service-three 3 | -------------------------------------------------------------------------------- /dev-resources/bootstrapping/cli/split_bootstraps/empty/empty1.cfg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puppetlabs/trapperkeeper/f7772765f17d3a0bc0d8af022d811eaa6cc0c539/dev-resources/bootstrapping/cli/split_bootstraps/empty/empty1.cfg -------------------------------------------------------------------------------- /dev-resources/bootstrapping/cli/split_bootstraps/empty/empty2.cfg: -------------------------------------------------------------------------------- 1 | # any entries here? 2 | 3 | # nope 4 | -------------------------------------------------------------------------------- /dev-resources/bootstrapping/cli/split_bootstraps/one/bootstrap_one.cfg: -------------------------------------------------------------------------------- 1 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/cli-test-service 2 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/hello-world-service 3 | -------------------------------------------------------------------------------- /dev-resources/bootstrapping/cli/split_bootstraps/spaces/bootstrap with spaces one.cfg: -------------------------------------------------------------------------------- 1 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/cli-test-service 2 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/hello-world-service 3 | -------------------------------------------------------------------------------- /dev-resources/bootstrapping/cli/split_bootstraps/spaces/bootstrap with spaces two.cfg: -------------------------------------------------------------------------------- 1 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/test-service-two 2 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/test-service-three 3 | -------------------------------------------------------------------------------- /dev-resources/bootstrapping/cli/split_bootstraps/two/bootstrap_two.cfg: -------------------------------------------------------------------------------- 1 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/test-service-two 2 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/test-service-three 3 | -------------------------------------------------------------------------------- /dev-resources/bootstrapping/cwd/bootstrap.cfg: -------------------------------------------------------------------------------- 1 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/cwd-test-service 2 | puppetlabs.trapperkeeper.examples.bootstrapping.test-services/hello-world-service 3 | -------------------------------------------------------------------------------- /dev-resources/bootstrapping/jar/this-jar-contains-a-bootstrap-config-file.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puppetlabs/trapperkeeper/f7772765f17d3a0bc0d8af022d811eaa6cc0c539/dev-resources/bootstrapping/jar/this-jar-contains-a-bootstrap-config-file.jar -------------------------------------------------------------------------------- /dev-resources/bootstrapping/plugin/bootstrap.cfg: -------------------------------------------------------------------------------- 1 | test-services.plugin-test-services/plugin-test-service 2 | -------------------------------------------------------------------------------- /dev-resources/config/conflictdir1/config.conf: -------------------------------------------------------------------------------- 1 | foo { 2 | // comment 3 | somesetting : 12 4 | # comment 5 | baz = "hi" 6 | } -------------------------------------------------------------------------------- /dev-resources/config/conflictdir1/config.ini: -------------------------------------------------------------------------------- 1 | [foo] 2 | bar = "barbar" 3 | baz = bazbaz -------------------------------------------------------------------------------- /dev-resources/config/conflictdir2/config.json: -------------------------------------------------------------------------------- 1 | {"foo": 2 | {"something": "something", 3 | "baz": "jsonbaz"}} -------------------------------------------------------------------------------- /dev-resources/config/conflictdir2/config.properties: -------------------------------------------------------------------------------- 1 | foo.bar="barbar" 2 | foo.baz=bazbaz -------------------------------------------------------------------------------- /dev-resources/config/conflictdir3/config.edn: -------------------------------------------------------------------------------- 1 | {:foo 2 | {:bar "barbar" 3 | :baz "bazbaz"}} -------------------------------------------------------------------------------- /dev-resources/config/conflictdir3/config.json: -------------------------------------------------------------------------------- 1 | {"foo": 2 | {"something": "something", 3 | "baz": "jsonbaz"}} -------------------------------------------------------------------------------- /dev-resources/config/file/config.conf: -------------------------------------------------------------------------------- 1 | foo { 2 | baz = "bazbaz" 3 | // this is a test comment 4 | bam: 42 5 | # this is another test comment 6 | bap.boozle = "boozleboozle" 7 | } 8 | foo.bar = barbar 9 | foo.bap : { 10 | bip : [1, 2, { hi = "there" }, 3] 11 | } -------------------------------------------------------------------------------- /dev-resources/config/file/config.edn: -------------------------------------------------------------------------------- 1 | {:foo 2 | {:bar "barbar" 3 | :baz "bazbaz" 4 | :bam 42 5 | :bap 6 | {:boozle "boozleboozle" 7 | :bip [1 2 {:hi "there"} 3]}}} -------------------------------------------------------------------------------- /dev-resources/config/file/config.ini: -------------------------------------------------------------------------------- 1 | [foo] 2 | 3 | # these are some settings 4 | setting1 = foo1 5 | setting2=foo2 6 | 7 | [bar] 8 | setting1 = bar1 -------------------------------------------------------------------------------- /dev-resources/config/file/config.json: -------------------------------------------------------------------------------- 1 | {"foo": 2 | {"bar": "barbar", 3 | "baz": "bazbaz", 4 | "bam": 42, 5 | "bap": 6 | {"boozle": "boozleboozle", 7 | "bip": [1, 2, {"hi": "there"}, 3] 8 | }}} -------------------------------------------------------------------------------- /dev-resources/config/file/config.properties: -------------------------------------------------------------------------------- 1 | foo.bar="barbar" 2 | foo.baz=bazbaz 3 | foo.bam=42 4 | foo.bap.boozle="boozleboozle" -------------------------------------------------------------------------------- /dev-resources/config/inidir/bam.ini: -------------------------------------------------------------------------------- 1 | [bam] 2 | setting1 = bam1 -------------------------------------------------------------------------------- /dev-resources/config/inidir/baz.ini: -------------------------------------------------------------------------------- 1 | [baz] 2 | 3 | # these are some settings 4 | setting1 = baz1 5 | setting2=baz2 6 | -------------------------------------------------------------------------------- /dev-resources/config/mixeddir/bar.conf: -------------------------------------------------------------------------------- 1 | bar { 2 | nesty.mappy { 3 | hi = there 4 | # comment 5 | stuff = [1, 2, {"how" = "areyou"}, 3] 6 | } 7 | // comment 8 | junk : "thingz" 9 | } -------------------------------------------------------------------------------- /dev-resources/config/mixeddir/baz.ini: -------------------------------------------------------------------------------- 1 | [baz] 2 | 3 | # these are some settings 4 | setting1 = baz1 5 | setting2=baz2 6 | -------------------------------------------------------------------------------- /dev-resources/config/mixeddir/foo.properties: -------------------------------------------------------------------------------- 1 | foo.bar="barbar" 2 | foo.baz=bazbaz 3 | foo.meaningoflife=42 -------------------------------------------------------------------------------- /dev-resources/config/mixeddir/taco.json: -------------------------------------------------------------------------------- 1 | {"taco": 2 | {"burrito": [1, 2], 3 | "nacho": "cheese"}} -------------------------------------------------------------------------------- /dev-resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d %-5p [%c{2}] %m%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /dev-resources/logging/logback-debug.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d %-5p [%c{2}] %m%n 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /dev-resources/logging/logback-evaluator-filter.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | matcher 8 | should get filtered 9 | 10 | matcher.matches(formattedMessage) 11 | 12 | NEUTRAL 13 | DENY 14 | 15 | 16 | 17 | 18 | omgMatcher 19 | OMGOMG 20 | 21 | omgMatcher.matches(throwable.getMessage()) 22 | 23 | NEUTRAL 24 | DENY 25 | 26 | 27 | ./target/test/logback-evaluator-filter-test.log 28 | false 29 | 30 | %d %-5p [%c{2}] %m%n 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /dev-resources/logging/logback-warn.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d %-5p [%c{2}] %m%n 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /documentation/Bootstrapping.md: -------------------------------------------------------------------------------- 1 | # Bootstrapping 2 | 3 | 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. 4 | 5 | ## `bootstrap.cfg` 6 | 7 | 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`: 8 | 9 | ``` 10 | puppetlabs.trapperkeeper.services.nrepl.nrepl-service/nrepl-service 11 | my.custom.namespace/foo-service 12 | ``` 13 | 14 | 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. 15 | 16 | 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: 17 | 18 | * 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 19 | * in the current working directory 20 | * on the classpath 21 | 22 | ## Configuration 23 | 24 | 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. 25 | -------------------------------------------------------------------------------- /documentation/Built-in-Configuration-Service.md: -------------------------------------------------------------------------------- 1 | # Trapperkeeper's Built-in Configuration Service 2 | 3 | The configuration service is built-in to Trapperkeeper and is always loaded. It performs the following tasks at application startup: 4 | 5 | * Reads all application configuration into memory 6 | * Initializes logging 7 | * Provides functions that can be injected into other services to give them access to the configuration data 8 | 9 | In its current form, the configuration service has some fairly rigid behavior though in the future we hope to make it more dynamic. 10 | 11 | ## Loading configuration data 12 | 13 | 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. 14 | 15 | We support several types of files for expressing the configuration data: 16 | 17 | * `.ini` files 18 | * `.edn` files (Clojure's [Extensible Data Notation](https://github.com/edn-format/edn) format) 19 | * `.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)) 20 | * `.json` files 21 | * `.properties` files 22 | 23 | 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.) 24 | 25 | Here's the protocol for the configuration service: 26 | 27 | ```clj 28 | (defprotocol ConfigService 29 | (get-config [this] "Returns a map containing all of the configuration values") 30 | (get-in-config [this ks] [this ks default] 31 | "Returns the individual configuration value from the nested 32 | configuration structure, where ks is a sequence of keys. 33 | Returns nil if the key is not present, or the default value if 34 | supplied.")) 35 | ``` 36 | 37 | Your service may then specify a dependency on the configuration service in order to access service configuration data. 38 | 39 | 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 40 | 41 | ```conf 42 | foosection1{ 43 | foosetting1 = foo 44 | foosetting2 = bar 45 | } 46 | ``` 47 | 48 | Then, you can define a service like this: 49 | 50 | ```clj 51 | (defservice foo-service 52 | [[:ConfigService get-in-config]] 53 | ;; service initialization code 54 | (init [this context] 55 | (println 56 | (format "foosetting2 has a value of '%s'" 57 | (get-in-config [:foosection1 :foosetting2]))) 58 | context)) 59 | ``` 60 | 61 | 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: 62 | 63 | foosetting2 has a value of 'bar' 64 | 65 | ## Logging configuration 66 | 67 | 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. 68 | 69 | 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. 70 | 71 | `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). 72 | 73 | For example: 74 | 75 | ```CONF 76 | global { 77 | logging-config = /path/to/logback.xml 78 | } 79 | ``` 80 | -------------------------------------------------------------------------------- /documentation/Built-in-Services.md: -------------------------------------------------------------------------------- 1 | # Built-in Services 2 | 3 | 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). 4 | 5 | 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. 6 | 7 | Detailed information about Trapperkeeper's built-in services can be found on the following pages: 8 | 9 | - [Configuration Service](Built-in-Configuration-Service.md) 10 | - [Shutdown Service](Built-in-Shutdown-Service.md) 11 | - [nREPL Service](Built-in-nREPL-Service.md) 12 | -------------------------------------------------------------------------------- /documentation/Built-in-Shutdown-Service.md: -------------------------------------------------------------------------------- 1 | # Trapperkeeper's Built-in Shutdown Service 2 | 3 | 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: 4 | 5 | * Listen for a shutdown signal to the process, and initiate shutdown of the application if one is received (via CTRL-C or TERM signal) 6 | * 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) 7 | 8 | ## Shutdown Hooks 9 | 10 | 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. 11 | 12 | For example: 13 | 14 | ```clj 15 | (defn bar-shutdown 16 | [] 17 | (log/info "bar-service shutting down!")) 18 | 19 | (defservice bar-service 20 | [[:FooService foo]] 21 | ;; service initialization code 22 | (init [this context] 23 | (log/info "bar-service initializing.") 24 | context) 25 | 26 | ;; shutdown code 27 | (stop [this context] 28 | (bar-shutdown) 29 | context)) 30 | ``` 31 | 32 | 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). 33 | 34 | ## Provided Shutdown Functions 35 | 36 | The shutdown service provides two functions that can be injected into other services: `request-shutdown` and `shutdown-on-error`. Here's the protocol: 37 | 38 | ```clj 39 | (defprotocol ShutdownService 40 | (request-shutdown [this] "Asynchronously trigger normal shutdown") 41 | (shutdown-on-error [this service-id f] [this service-id f on-error] 42 | "Higher-order function to execute application logic and trigger shutdown in 43 | the event of an exception")) 44 | ``` 45 | 46 | To use them, you may simply specify a dependency on them: 47 | 48 | ```clj 49 | (defservice baz-service 50 | [[:ShutdownService request-shutdown shutdown-on-error]] 51 | ;; ... 52 | ) 53 | ``` 54 | 55 | ### `request-shutdown` 56 | 57 | `request-shutdown` initiates a shutdown of the application container 58 | which will, in turn, cause all registered shutdown hooks to be called. 59 | It is asynchronous and will eventually cause the `run` function to 60 | return. 61 | 62 | It accepts an optional argument which can be used to provide a map 63 | specifying a process exit status and final messages like this: 64 | 65 | ```clj 66 | {:puppetlabs.trapperkepper.core/exit` 67 | {:status 3 68 | :messages [["Unexpected filesystem error ..." *err*]]}} 69 | ``` 70 | 71 | which will finally be thrown from `run` as an `ex-info` of `:kind` 72 | `:puppetlabs.trapperkepper.core/exit` like this: 73 | 74 | 75 | ```clj 76 | {:kind :puppetlabs.trapperkepper.core/exit` 77 | :status 3 78 | :messages [["Unexpected filesystem error ..." *err*]]}} 79 | ``` 80 | 81 | The `:messages` should include any desired newlines, and when relying 82 | on `:puppetlabs.trapperkepper.core/main`, the `:messages` will be 83 | printed and `exit` will be called with the given `:status`. 84 | 85 | ### `shutdown-on-error` 86 | 87 | `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: 88 | 89 | ```clj 90 | (try 91 | ; execute the given function 92 | (catch Throwable t 93 | ; initiate Trapperkeeper's shutdown logic 94 | ``` 95 | This has two main use-cases: 96 | * "worker" / background threads that your service may launch 97 | * 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 98 | 99 | `shutdown-on-error` accepts either two or three arguments: `[service-id f]` or `[service-id f on-error-fn]`. 100 | 101 | `service-id` is the id of your service; you can retrieve this via `(service-id this)` inside of any of your service function definitions. 102 | 103 | `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). 104 | 105 | Here's an example: 106 | 107 | ```clj 108 | (defn my-work-fn 109 | [] 110 | ;; do some work 111 | (Thread/sleep 10000) 112 | ;; uh-oh! An unhandled exception! 113 | (throw (IllegalStateException. "egads!"))) 114 | 115 | (defn my-error-cleanup-fn 116 | [context] 117 | (log/info "Something terrible happened! Foo: " (context :foo)) 118 | (log/info "Performing shutdown logic that should only happen on a fatal error.")) 119 | 120 | (defn my-normal-shutdown-fn 121 | [] 122 | (log/info "Performing normal shutdown logic.")) 123 | 124 | (defservice yet-another-service 125 | [[:ShutdownService shutdown-on-error]] 126 | (init [this context] 127 | (assoc context 128 | :worker-thread 129 | (future (shutdown-on-error (service-id this) my-work-fn my-error-cleanup-fn)))) 130 | 131 | (stop [this context] 132 | (my-normal-shutdown-fn) 133 | context)) 134 | ``` 135 | 136 | 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`). 137 | -------------------------------------------------------------------------------- /documentation/Built-in-nREPL-Service.md: -------------------------------------------------------------------------------- 1 | # Configuring the nREPL service 2 | 3 | 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. 4 | 5 | The `nrepl` section in a _Trapperkeeper_ configuration file specifies all the settings needed to start up an `nREPL` server attached to _Trapperkeeper_. 6 | 7 | ## `boostrap.cfg` 8 | 9 | By default, the nrepl service is not put into your application's `bootstrap.cfg`. If you want to use this service, add 10 | `puppetlabs.trapperkeeper.services.nrepl.nrepl-service/nrepl-service` to your `bootstrap.cfg` and enable it in your config. 11 | 12 | ## `enabled` 13 | 14 | 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. 15 | 16 | ## `host` 17 | 18 | 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. 19 | 20 | ## `port` 21 | 22 | The port that the `nREPL` server is bound to. If no port is defined then the default value of `7888` is used. 23 | 24 | ## `middlewares` 25 | 26 | A list of nREPL middlewares to load; for example, for compatibility with LightTable or other editors. 27 | 28 | ## Typical `config.conf` for nREPL 29 | 30 | ```conf 31 | nrepl { 32 | port = 12345 33 | enabled = true 34 | middlewares = [lighttable.nrepl.handler/lighttable-ops] 35 | } 36 | ``` 37 | 38 | ## The `nREPL` server 39 | 40 | For more information on the nREPL server see [the nREPL server README](https://github.com/clojure/tools.nrepl/blob/master/README.md). 41 | -------------------------------------------------------------------------------- /documentation/Command-Line-Arguments.md: -------------------------------------------------------------------------------- 1 | # Command Line Arguments 2 | 3 | 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: 4 | 5 | * It needs some data for bootstrapping 6 | * 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 7 | * Who wants to process command-line arguments, anyway? 8 | 9 | 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. 10 | 11 | Trapperkeeper supports four command-line arguments: 12 | 13 | * `--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. 14 | * `--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) 15 | * `--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. 16 | * `--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. 17 | 18 | ### Multiple bootstrap files 19 | 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. 20 | 21 | 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. 22 | 23 | Multiple bootstrap files are specified by giving the `--bootstrap-config` command line option a comma separated list of files and directories. For example: 24 | ``` 25 | --bootstrap-config ./first/path,/etc/second/path,./a/single/file.cfg 26 | ``` 27 | 28 | Each item in the list of paths can be one of: 29 | * A path to a single config file 30 | * A path to a directory of config files. Only files ending in .cfg will be used 31 | * A path to a file inside of a jar. E.g. `jar:file:///usr/bin/myjar.jar!/bootstrap.cfg` 32 | 33 | ## `main` and Trapperkeeper 34 | 35 | There are three different ways that you can initiate Trapperkeeper's bootstrapping process: 36 | 37 | ### Defer to Trapperkeeper's `main` function 38 | 39 | In your Leiningen project file, you can simply specify Trapperkeeper's `main` as your `:main`: 40 | 41 | :main puppetlabs.trapperkeeper.main 42 | 43 | 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`. 44 | 45 | ### Call Trapperkeeper's `main` function from your code 46 | 47 | 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: 48 | 49 | ```clj 50 | (ns foo 51 | (:require [puppetlabs.trapperkeeper.core :as trapperkeeper])) 52 | 53 | (defn -main 54 | [& args] 55 | ;; ... any code you like goes here 56 | (apply trapperkeeper/main args)) 57 | ``` 58 | 59 | Trapperkeeper's `main` will call `exit` itself in some cases, 60 | e.g. after argument processing errors, `--help` requests, or calls to 61 | `request-shutdown` that specify a specific process exit status. 62 | 63 | ### Call Trapperkeeper's `run` function directly 64 | 65 | 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. 66 | 67 | *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. 68 | 69 | But, if you absolutely must... Here's how it can be done: 70 | 71 | ```clj 72 | (ns foo 73 | (:require [puppetlabs.trapperkeeper.core :as trapperkeeper])) 74 | 75 | (defn -main 76 | [& args] 77 | (let [my-processed-cli-args (process-cli-args args) 78 | trapperkeeper-options {:config (my-processed-cli-args :config-file-path) 79 | :bootstrap-config nil 80 | :debug false}] 81 | ;; ... other app initialization code 82 | (trapperkeeper/run trapperkeeper-options))) 83 | ``` 84 | 85 | 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. 86 | 87 | If shutdown is initiatiated by a call to `request-shutdown` asking for 88 | a specific exit status, `run` will throw an ex-info exception with a 89 | `:kind` of `puppetlabs.trapperkeeper.core/exit`. See the 90 | `request-shutdown` documentation for additional information. 91 | 92 | ### Other Ways to Boot 93 | 94 | 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. 95 | 96 | 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. 97 | 98 | #### `TrapperkeeperApp` protocol 99 | 100 | There is a protocol that represents a Trapperkeeper application: 101 | 102 | ```clj 103 | (defprotocol TrapperkeeperApp 104 | "Functions available on a Trapperkeeper application instance" 105 | (app-context [this] "Returns the application context for this app (an atom containing a map)") 106 | (check-for-errors! [this] (str "Check for any errors which have occurred in " 107 | "the bootstrap process. If any have " 108 | "occurred, throw a `java.lang.Throwable` with " 109 | "the contents of the error. If none have " 110 | "occurred, return the input parameter.") 111 | (init [this] "Initialize the services") 112 | (start [this] "Start the services") 113 | (stop [this] "Stop the services")) 114 | ``` 115 | 116 | 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: 117 | 118 | * `(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. 119 | * `(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. 120 | * `(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. 121 | 122 | 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. 123 | 124 | 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: 125 | 126 | * `(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`. 127 | -------------------------------------------------------------------------------- /documentation/Configuring-the-nREPL-Service.md: -------------------------------------------------------------------------------- 1 | # Configuring the nREPL service 2 | 3 | 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. 4 | 5 | The `[nrepl]` section in a _Trapperkeeper_ `.ini` configuration file specifies all the settings needed to start up an `nREPL` server attached to _Trapperkeeper_. 6 | 7 | ## `enabled` 8 | 9 | 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. 10 | 11 | ## `host` 12 | 13 | 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. 14 | 15 | ## `port` 16 | 17 | The port that the `nREPL` server is bound to. If no port is defined then the default value of `7888` is used. 18 | 19 | ## `middlewares` 20 | 21 | A list of nREPL middlewares to load; for example, for compatibility with LightTable or other editors. 22 | 23 | ## Typical `config.ini` for nREPL 24 | 25 | ```ini 26 | [nrepl] 27 | port = 12345 28 | enabled = true 29 | middlewares = [lighttable.nrepl.handler/lighttable-ops] 30 | ``` 31 | 32 | ## The `nREPL` server 33 | 34 | For more information on the nREPL server, see [the tools.nrepl README](https://github.com/clojure/tools.nrepl/blob/master/README.md). 35 | -------------------------------------------------------------------------------- /documentation/Defining-Services.md: -------------------------------------------------------------------------------- 1 | # Defining Services 2 | 3 | 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: 4 | 5 | ## `defservice` 6 | 7 | `defservice` takes the following arguments: 8 | 9 | * a service name 10 | * an optional doc string 11 | * an optional service protocol; only required if your service exports functions that can be used by other services 12 | * a dependency list indicating other services/functions that this service requires 13 | * 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. 14 | 15 | ### Service Lifecycle 16 | 17 | The service `Lifecycle` protocol looks like this: 18 | 19 | ```clj 20 | (defprotocol Lifecycle 21 | (init [this context]) 22 | (start [this context]) 23 | (stop [this context])) 24 | ``` 25 | 26 | (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.) 27 | 28 | 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. 29 | 30 | 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. 31 | 32 | 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. 33 | 34 | ### Example Service 35 | 36 | Let's look at a concrete example: 37 | 38 | ```clj 39 | ;; This is the list of functions that the `FooService` must implement, and which 40 | ;; are available to other services who have a dependency on `FooService`. 41 | (defprotocol FooService 42 | (foo1 [this x]) 43 | (foo2 [this]) 44 | (foo3 [this x])) 45 | 46 | (defservice foo-service 47 | ;; docstring (optional) 48 | "A service that foos." 49 | 50 | ;; now we specify the (optional) protocol that this service satisfies: 51 | FooService 52 | 53 | ;; the :depends value should be a vector of vectors. Each of the inner vectors 54 | ;; should begin with a keyword that matches the protocol name of another service, 55 | ;; which may be followed by any number of symbols. Each symbol is the name of a 56 | ;; function that is provided by that service. Trapperkeeper will fail fast at 57 | ;; startup if any of the specified dependency services do not exist, *or* if they 58 | ;; do not provide all of the functions specified in your vector. (Note that 59 | ;; the syntax used here is actually just the 60 | ;; [fnk binding syntax from the Plumatic plumbing library](https://github.com/plumatic/plumbing/tree/master/src/plumbing/fnk#fnk-syntax), 61 | ;; so you can technically use any form that is compatible with that.) 62 | [[:SomeService function1 function2] 63 | [:AnotherService function3 function4]] 64 | 65 | ;; After your dependencies list comes the function implementations. 66 | ;; You must implement all of the protocol functions (if a protocol is 67 | ;; specified), and you may also override any `Lifecycle` functions that 68 | ;; you choose. We'll start by implementing the `init` function from 69 | ;; the `Lifecycle`: 70 | (init [this context] 71 | ;; do some initialization 72 | ;; ... 73 | ;; now return the service context map; we can update it to include 74 | ;; some state if we like. Note that we can use the functions that 75 | ;; were specified in our dependency list here: 76 | (assoc context :foo (str "Some interesting state:" (function1))) 77 | 78 | ;; We could optionally also override the `start` and `stop` lifecycle 79 | ;; functions, but we won't for this example. 80 | 81 | ;; Now we'll define our service function implementations. Again, we are 82 | ;; free to use the imported functions from the other services here: 83 | (foo1 [this x] ((comp function2 function3) x)) 84 | (foo2 [this] (println "Function4 returns" (function4))) 85 | 86 | ;; We can also access the service context that we updated during the 87 | ;; lifecycle functions, by using the `service-context` function from 88 | ;; the `Service` protocol: 89 | (foo3 [this x] 90 | (let [context (service-context this)] 91 | (format "x + :foo is: '%s'" (str x (:foo context)))))) 92 | ``` 93 | 94 | 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. 95 | 96 | ### Multi-arity Protocol Functions 97 | 98 | Clojure's protocols allow you to define multi-arity functions: 99 | 100 | ```clj 101 | (defprotocol MultiArityService 102 | (foo [this x] [this x y])) 103 | ``` 104 | 105 | Trapperkeeper services can use the syntax from clojure's `reify` to implement these multi-arity functions: 106 | 107 | ```clj 108 | (defservice my-service 109 | MultiArityService 110 | [] 111 | (foo [this x] x) 112 | (foo [this x y] (+ x y))) 113 | ``` 114 | 115 | ## `service` 116 | 117 | `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): 118 | 119 | ```clj 120 | (service 121 | [] 122 | (init [this context] 123 | (println "Starting anonymous service!") 124 | context)) 125 | 126 | (defprotocol AnotherService 127 | (foo [this])) 128 | ``` 129 | 130 | ## Optional Services 131 | 132 | _Introduced in Trapperkeeper 1.2.0_ 133 | 134 | 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. 135 | 136 | To mark a dependency as optional, you use a different form to specify your dependencies: 137 | 138 | ```clj 139 | (defprotocol HaikuService 140 | (get-haiku [this] "return a lovely haiku")) 141 | (defprotocol SonnetService 142 | (get-sonnet [this] "return a lovely sonnet")) 143 | 144 | ;; ... snip the definitions of HaikuService and SonnetService ... 145 | 146 | (defservice poetry-service 147 | PoetryService 148 | {:required [HaikuService] 149 | :optional [SonnetService]} 150 | (haiku [this] 151 | (get-haiku HaikuService)) 152 | (sonnet [this] 153 | (if-let [sonnet-svc (tk-svc/maybe-get-service this :SonnetService)] 154 | (get-sonnet sonnet-svc) 155 | "insert moving sonnet here")) 156 | ``` 157 | 158 | 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"`. 159 | 160 | **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. 161 | 162 | The `Service` protocol has two helpers to make it easier to work with optional dependencies: 163 | 164 | * `(maybe-get-service [this service-id])` which takes a keyword service ID and returns the service, if included, or nil 165 | * `(service-included? [this service-id])` which takes a keyword service ID and returns true or false based on its inclusion. 166 | 167 | These helpers live alongside the other service helpers like `get-service` in `puppetlabs.trapperkeeper.services`. 168 | 169 | ## Referencing Services 170 | 171 | To learn how to refer to services in the rest of your application, head over to the [Referencing Services](Referencing-Services.md) page. 172 | -------------------------------------------------------------------------------- /documentation/Error-Handling.md: -------------------------------------------------------------------------------- 1 | # Error Handling 2 | 3 | ## Errors During `init` or `start` 4 | 5 | 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. 6 | 7 | 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. 8 | 9 | ## Services Should Fail Fast 10 | 11 | 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). 12 | -------------------------------------------------------------------------------- /documentation/Helpful-Leiningen-Features.md: -------------------------------------------------------------------------------- 1 | # Helpful Leiningen Features 2 | 3 | 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: 4 | 5 | ### Leiningen's `checkouts` feature 6 | 7 | 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. 8 | 9 | ### Leiningen's `trampoline` feature 10 | 11 | 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`. 12 | 13 | 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`. 14 | -------------------------------------------------------------------------------- /documentation/Index.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/puppetlabs/trapperkeeper.png?branch=master)](https://travis-ci.org/puppetlabs/trapperkeeper) 2 | 3 | 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. 4 | 5 | * [Overview](Overview.md) 6 | * [Credits and Origins](Overview.md#credits-and-origins) 7 | * [Hopes and Dreams](Overview.md#hopes-and-dreams) 8 | * [Trapperkeeper Quick Start](Trapperkeeper-Quick-Start.md) 9 | * [Leiningen Template](Trapperkeeper-Quick-Start.md#lein-template) 10 | * [Hello World](Trapperkeeper-Quick-Start.md#hello-world) 11 | * [Defining Services](Defining-Services.md) 12 | * [`defservice`](Defining-Services.md#defservice) 13 | * [Service Lifecycle](Defining-Services.md#service-lifecycle) 14 | * [Example Service](Defining-Services.md#example-service) 15 | * [Multi-arity Protocol Functions](Defining-Services.md#multi-arity-protocol-functions) 16 | * [`service`](Defining-Services.md#service) 17 | * [Optional Services](Defining-Services.md#optional-services) 18 | * [Referencing Services](Referencing-Services.md) 19 | * [Individual Functions](Referencing-Services.md#individual-functions) 20 | * [A Map of Functions](Referencing-Services.md#a-map-of-functions) 21 | * [Plumatic Graph Binding Form](Referencing-Services.md#plumatic-graph-binding-form) 22 | * [Via Service Protocol](Referencing-Services.md#via-service-protocol) 23 | * [Bootstrapping](Bootstrapping.md) 24 | * [Built-in Services](Built-in-Services.md) 25 | * [Configuration Service](Built-in-Configuration-Service.md) 26 | * [Shutdown Service](Built-in-Shutdown-Service.md) 27 | * [nREPL Service](Built-in-nREPL-Service.md) 28 | * [Error Handling](Error-Handling.md) 29 | * [Service Interfaces](Service-Interfaces.md) 30 | * [Command Line Arguments](Command-Line-Arguments.md) 31 | * [Other Ways to Boot](Command-Line-Arguments.md#other-ways-to-boot) 32 | * [Restart File Feature for Determining When Services Have Been Started](Restart-File.md) 33 | * [Test Utils](Test-Utils.md) 34 | * [Trapperkeeper Best Practices](Trapperkeeper-Best-Practices.md) 35 | * [To Trapperkeeper Or Not To Trapperkeeper](Trapperkeeper-Best-Practices.md#to-trapperkeeper-or-not-to-trapperkeeper) 36 | * [Separating Logic From Service Definitions](Trapperkeeper-Best-Practices.md#separating-logic-from-service-definitions) 37 | * [On Lifecycles](Trapperkeeper-Best-Practices.md#on-lifecycles) 38 | * [Testing Services](Trapperkeeper-Best-Practices.md#testing-services) 39 | * [Using the "Reloaded" Pattern](Reloaded-Pattern.md) 40 | * [Experimental Plugin System](Plugin-System.md) 41 | * [Polyglot Support](Polyglot-Support.md) 42 | * [Helpful Leiningen Features](Helpful-Leiningen-Features.md) 43 | -------------------------------------------------------------------------------- /documentation/Overview.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | 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. 4 | 5 | It ties together a few nice patterns we've come across in the clojure community: 6 | 7 | * Stuart Sierra's ["reloaded" workflow](http://thinkrelevance.com/blog/2013/06/04/clojure-workflow-reloaded) 8 | * Component lifecycles (["Component"](https://github.com/stuartsierra/component), ["jig"](https://github.com/juxt/jig#components)) 9 | * [Composable services](http://plumatic.github.io/graph-abstractions-for-structured-computation/) (based on the excellent [Plumamatic graph library](https://github.com/plumatic/plumbing)) 10 | 11 | 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): 12 | 13 | * Well-defined service interfaces (using clojure protocols) 14 | * Ability to turn services on and off via configuration after deploy 15 | * Ability to swap service implementations via configuration after deploy 16 | * Ability to load multiple web apps (usually Ring) into a single webserver 17 | * Unified initialization of logging and configuration so services don't have to concern themselves with the implementation details 18 | * Super-simple configuration syntax 19 | 20 | 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. 21 | 22 | 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. 23 | 24 | # Credits and Origins 25 | 26 | 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! 27 | 28 | 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. 29 | 30 | # Hopes and Dreams 31 | 32 | Here are some ideas that we've had and things we've played around with a bit for improving Trapperkeeper in the future. 33 | 34 | ## More flexible configuration service 35 | 36 | 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. 37 | 38 | ## Alternate implementations of the webserver service 39 | 40 | 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. 41 | 42 | ## Add support for other types of web applications 43 | 44 | 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). 45 | -------------------------------------------------------------------------------- /documentation/Plugin-System.md: -------------------------------------------------------------------------------- 1 | # Experimental Plugin System 2 | 3 | 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. 4 | 5 | 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. 6 | 7 | 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. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /documentation/Polyglot-Support.md: -------------------------------------------------------------------------------- 1 | # Polyglot Support 2 | 3 | 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). 4 | 5 | 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. 6 | 7 | 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. 8 | 9 | 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). 10 | -------------------------------------------------------------------------------- /documentation/Referencing-Services.md: -------------------------------------------------------------------------------- 1 | # Referencing Services 2 | 3 | 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. 4 | 5 | ## Individual Functions 6 | 7 | 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: 8 | 9 | ```clj 10 | (defservice foo-service 11 | [[:BarService bar-fn] 12 | [:BazService baz-fn]] 13 | (init [this context] 14 | (bar-fn) 15 | (baz-fn) 16 | context)) 17 | ``` 18 | 19 | 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. 20 | 21 | ## A Map of Functions 22 | 23 | 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: 24 | 25 | ```clj 26 | (defservice foo-service 27 | [BarService BazService] 28 | (init [this context] 29 | ((:bar-fn BarService)) 30 | ((:baz-fn BazService)) 31 | context)) 32 | ``` 33 | 34 | 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. 35 | 36 | ## Plumatic Graph Binding Form 37 | 38 | 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). 39 | 40 | ## Via Service Protocol 41 | 42 | 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: 43 | 44 | ```clj 45 | (ns bar.service) 46 | 47 | (defprotocol BarService 48 | (bar-fn [this])) 49 | 50 | ... 51 | 52 | (ns foo.service 53 | (:require [bar.service :as bar])) 54 | 55 | (defservice foo-service 56 | ;; This dependency is only here to enforce that the BarService gets loaded 57 | ;; before this service does; we won't need to refer to the `BarService` var 58 | ;; anywhere in this service definition. 59 | [BarService] 60 | (init [this context] 61 | (let [bar-service (get-service this :BarService)] 62 | (bar/bar-fn bar-service)) 63 | context)) 64 | ``` 65 | -------------------------------------------------------------------------------- /documentation/Reloaded-Pattern.md: -------------------------------------------------------------------------------- 1 | # Using the "Reloaded" Pattern 2 | 3 | [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. 4 | 5 | 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: 6 | 7 | ```clj 8 | (ns examples.my-app.repl 9 | (:require [puppetlabs.trapperkeeper.services.webserver.jetty9-service 10 | :refer [jetty9-service]] 11 | [examples.my-app.services 12 | :refer [count-service foo-service baz-service]] 13 | [puppetlabs.trapperkeeper.core :as tk] 14 | [puppetlabs.trapperkeeper.app :as tka] 15 | [clojure.tools.namespace.repl :refer (refresh)])) 16 | 17 | ;; a var to hold the main `TrapperkeeperApp` instance. 18 | (def system nil) 19 | 20 | (defn init [] 21 | (alter-var-root #'system 22 | (fn [_] (tk/build-app 23 | [jetty9-service 24 | count-service 25 | foo-service 26 | baz-service] 27 | {:global 28 | {:logging-config "examples/my_app/logback.xml"} 29 | :webserver {:port 8080} 30 | :example {:my-app-config-value "FOO"}}))) 31 | (alter-var-root #'system tka/init) 32 | (tka/check-for-errors! system)) 33 | 34 | (defn start [] 35 | (alter-var-root #'system 36 | (fn [s] (if s (tka/start s)))) 37 | (tka/check-for-errors! system)) 38 | 39 | (defn stop [] 40 | (alter-var-root #'system 41 | (fn [s] (when s (tka/stop s))))) 42 | 43 | (defn go [] 44 | (init) 45 | (start)) 46 | 47 | (defn context [] 48 | @(tka/app-context system)) 49 | 50 | ;; pretty print the entire application context 51 | (defn print-context [] 52 | (clojure.pprint/pprint (context))) 53 | 54 | (defn reset [] 55 | (stop) 56 | (refresh :after 'examples.my-app.repl/go)) 57 | ``` 58 | 59 | 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). 60 | -------------------------------------------------------------------------------- /documentation/Restart-File.md: -------------------------------------------------------------------------------- 1 | # Experimental Feature: Restart File 2 | 3 | When using Trapperkeeper apps inside of packages, it is convenient for a service 4 | framework to have a clear indication as to when all of the Trapperkeeper 5 | services in the app have been started -- as opposed to just knowing when the 6 | Java process hosting the app has been spawned. The "restart file" feature in 7 | Trapperkeeper provides this capability. As a reference example, the 8 | [EZBake](https://github.com/puppetlabs/ezbake) build system for 9 | Trapperkeeper-based applications makes use of the "restart file" feature. Its 10 | service packages can pause a hosting service framework (SysVinit, systemd) 11 | during a "service start" attempt until the app's services have all been started, 12 | or have failed to start. 13 | 14 | The "restart file" is considered to be a somewhat experimental feature in that 15 | the implementation may change in a future release. 16 | 17 | ## Implementation Details 18 | 19 | Each time Trapperkeeper has successfully finished processing all of the start 20 | calls that it makes to each of the services in an application -- both at Java 21 | process start and after a service reload is requested -- it increments a 22 | counter in a file on disk. The location of the file is controlled by the value 23 | of the `restart-file` setting. 24 | 25 | If the value in the file before services are started is '3', for example, the 26 | value will be updated to '4' after services have been started. If the file does 27 | not exist at the time services have been started, the value is written as '1'. 28 | The value rolls back around to '1' if the value would be incremented beyond the 29 | maximum value for a `java.lang.Long` or if the contents of the file is otherwise 30 | unable to be parsed as an integer. 31 | 32 | In terms of using the restart file as an indication that services have been 33 | started -- for example, from a background script that accesses the file in a 34 | polling loop to determine when the start phase has finished -- it is best to 35 | just look for a change to the contents of the file rather than having any 36 | specific logic that interprets the integer values. As noted earlier, the 37 | nature of the 'start' marker may change in a future release. 38 | 39 | ## Configuration Details 40 | 41 | The `restart-file` setting can be specified either via a command line argument 42 | to Trapperkeeper... 43 | 44 | ``` 45 | -r | --restart-file /write/file/here 46 | ``` 47 | 48 | ... or as a setting under the "global" section of a Trapperkeeper configuration 49 | file. For example, a HOCON-formatted "global.conf" might have: 50 | 51 | ``` 52 | global: { 53 | restart-file: /write/file/here 54 | } 55 | ``` 56 | 57 | In the event that the `restart-file` setting were specified both as a command 58 | line argument and within the "global" section of a Trapperkeeper configuration 59 | file, the value specified on the command line would be the one in which the 60 | 'start' counter is incremented. 61 | 62 | If a value for the `restart-file` setting is not specified via either the 63 | command line or within the "global" section of a Trapperkeeper configuration 64 | file, Trapperkeeper will not write a 'start' counter to any file. 65 | -------------------------------------------------------------------------------- /documentation/Service-Interfaces.md: -------------------------------------------------------------------------------- 1 | ## Service Interfaces 2 | 3 | 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. 4 | 5 | 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. 6 | 7 | Here's a concrete example of how this might work: 8 | 9 | ```clj 10 | (ns services.foo) 11 | 12 | (defprotocol FooService 13 | (foo [this])) 14 | 15 | (ns services.foo.lowercase-foo 16 | (:require [services.foo :refer [FooService]) 17 | 18 | (defservice foo-service 19 | "A lower-case implementation of the `foo-service`" 20 | FooService 21 | [] 22 | (foo [this] "foo")) 23 | 24 | (ns services.foo.uppercase-foo 25 | (:require [services.foo :refer [FooService])) 26 | 27 | (defservice foo-service 28 | "An upper-case implementation of the `foo-service`" 29 | FooService 30 | [] 31 | (foo [this] "FOO")) 32 | 33 | (ns services.foo-consumer) 34 | 35 | (defprotocol FooConsumer 36 | (bar [this])) 37 | 38 | (defservice foo-consumer 39 | "A service that consumes the `foo-service`" 40 | FooConsumer 41 | [[:FooService foo]] 42 | (bar [this] 43 | (format "Foo service returned: '%s'" (foo)))) 44 | ``` 45 | 46 | Given this combination of services, you might have a `bootstrap.cfg` file that looks like: 47 | 48 |
49 | services.foo-consumer/foo-consumer
50 | services.foo.lowercase-foo/foo-service
51 | 
52 | 53 | 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: 54 | 55 |
56 | services.foo-consumer/foo-consumer
57 | services.foo.uppercase-foo/foo-service
58 | 
59 | 60 | 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. 61 | 62 | 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. 63 | -------------------------------------------------------------------------------- /documentation/Test-Utils.md: -------------------------------------------------------------------------------- 1 | # Trapperkeeper Test Utils 2 | 3 | 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 4 | on by using a classifier in your project dependencies. 5 | 6 | ```clojure 7 | (defproject yourproject "1.0.0" 8 | ... 9 | :profiles {:dev {:dependencies [[puppetlabs/trapperkeeper "x.y.z" :classifier "test"]]}}) 10 | ``` 11 | 12 | ## Logging 13 | 14 | The 15 | [logging namespace](https://github.com/puppetlabs/trapperkeeper/tree/master/test/puppetlabs/trapperkeeper/testutils/logging.clj) 16 | provides utilities to help capture and validate logging behavior. 17 | 18 | ### `with-test-logging` 19 | 20 | This form provides one of the simplest, though least discriminating 21 | ways to examine the log events produced by a body of code. All log 22 | events generated by the "root" logger from within the form (typically 23 | all events) will be available for inspection by the `logged?` 24 | predicate: 25 | 26 | ```clojure 27 | (with-test-logging 28 | (log/info "hello log") 29 | (is (logged? #"^hello log$")) 30 | (is (logged? #"^hello log$" :info))) 31 | ``` 32 | 33 | Here `(log/info "hello log")` generates an info level log event with a 34 | message of "hello log", and then `logged?` checks for it, first by 35 | matching the message, and then by matching both the message and the 36 | level. 37 | 38 | ### `logged?` 39 | 40 | `logged?` must be called from within a `with-test-logging` form, and 41 | returns true if any events that match its arguments have been logged 42 | since the beginning of the form. 43 | 44 | See the `logged?` docstring for a complete description, but as an 45 | example, if the first argument is a regex pattern (typically generated 46 | via Clojure's `#"pattern"`), then `logged?` will return true if the 47 | pattern matches a single message of anything that has been logged since the 48 | beginning of the enclosing `with-test-logging` form. An optional 49 | second parameter restricts the match to log events with the specified 50 | level: `:trace`, `:debug`, `:info`, `:warn`, `:error` or `:fatal`. 51 | 52 | Note: by default `logged?` returns true only if there is exactly one 53 | log line match. An optional third parameter can be specified to disable 54 | this restriction. 55 | 56 | ### `event->map` 57 | 58 | This function converts a LogEvent to a Clojure map of the kind 59 | generated by `with-logged-event-maps` and `with-logger-event-maps`. A 60 | log event produced by `(log/info "hello log")` would be converted to 61 | this: 62 | 63 | ```clojure 64 | {:message "hello log" 65 | :level :info 66 | :exception nil 67 | :logger "the.namespace.containing.the.log.info.call"} 68 | ``` 69 | 70 | ### `with-logged-event-maps` 71 | 72 | This form provides more control than `with-test-logging` by appending 73 | an `event->map` map to a collection for each log event produced within 74 | its body, and the collection can be accessed though an atom bound to a 75 | caller-specified name. For example, the `with-test-logging` based 76 | tests above could be rewritten like this: 77 | 78 | ```clojure 79 | (with-logged-event-maps events 80 | (log/info "hello log") 81 | (is (some #(re-matches #"hello log" (:message %)) @events)) 82 | (is (some #(and (re-matches #"hello log" (:message %)) 83 | (= :info (:message %))) 84 | @events))) 85 | ``` 86 | 87 | A call to `(with-logged-event-maps ...)` is effectively the same as 88 | `(with-logger-event-maps root-logger-name ...)`. 89 | 90 | ### `with-logger-event-maps` 91 | 92 | This form is identical to `with-logged-event-maps` except that it 93 | allows the specification of the `logger-id` from which events should 94 | be captured; `with-logged-event-maps` always captures events from 95 | `root-logger-name`. 96 | 97 | ## Testing Services 98 | 99 | 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. 100 | 101 | 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. 102 | 103 | 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: 104 | 105 | * Making sure that no JVM shutdown hooks are registered during tests, as they would be during a normal Trapperkeeper application boot sequence 106 | * Making sure that the app is shut down properly after the test completes. 107 | 108 | Here are some of the most useful ones: 109 | 110 | ### `with-app-with-config` 111 | 112 | 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. 113 | 114 | ```clj 115 | (ns services.test-service-1) 116 | 117 | (defprotocol TestService1 118 | (test-fn [this])) 119 | 120 | (defservice test-service1 121 | TestService1 122 | [] 123 | (test-fn [this] "foo")) 124 | ``` 125 | ```clj 126 | (ns services.test-service2) 127 | 128 | (defservice test-service2 129 | ;;... 130 | ) 131 | ``` 132 | ```clj 133 | (ns test.services-test 134 | (:require services.test-service-1 :as t1)) 135 | 136 | (with-app-with-config app 137 | [test-service1 test-service2] 138 | {:myconfig {:foo "foo" 139 | :bar "bar"}} 140 | (let [test-svc (get-service app :TestService1)] 141 | (is (= "baz" (t1/test-fn test-svc)))) 142 | ``` 143 | 144 | ### `with-app-with-cli-data` 145 | 146 | 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: 147 | 148 | ```clj 149 | (with-app-with-cli-data app 150 | [test-service1 test-service2] 151 | {:config "./dev-resources/config.conf"} 152 | (let [test-svc (get-service app :TestService1)] 153 | (is (= "baz" (t1/test-fn test-svc)))) 154 | ``` 155 | 156 | ### `with-app-with-cli-args` 157 | 158 | This version accepts a vector of command-line arguments: 159 | 160 | ```clj 161 | (with-app-with-cli-args app 162 | [test-service1 test-service2] 163 | ["--config" "./dev-resources/config.conf" "--debug"] 164 | (let [test-svc (get-service app :TestService1)] 165 | (is (= "baz" (t1/test-fn test-svc)))) 166 | ``` 167 | 168 | ### `with-app-with-empty-config` 169 | 170 | This version is useful when you don't need to pass in any configuration data at all to the services: 171 | 172 | ```clj 173 | (with-app-with-empty-config app 174 | [test-service1 test-service2] 175 | (let [test-svc (get-service app :TestService1)] 176 | (is (= "baz" (t1/test-fn test-svc)))) 177 | ``` 178 | 179 | 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. 180 | -------------------------------------------------------------------------------- /documentation/Trapperkeeper-Best-Practices.md: -------------------------------------------------------------------------------- 1 | # Trapperkeeper Best Practices 2 | 3 | This page provides some general guidelines for writing Trapperkeeper services. 4 | 5 | ## To Trapperkeeper Or Not To Trapperkeeper 6 | 7 | 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: 8 | 9 | * 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. 10 | * 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 11 | * Your code has a need for a managed lifecycle; initialization / startup, shutdown / cleanup 12 | * Your code has a dependency on some other code that has a managed lifecycle 13 | * Your code requires external configuration that you would like to make consistent with other puppetlabs / Trapperkeeper applications 14 | 15 | ## Separating Logic From Service Definitions 16 | 17 | 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: 18 | 19 | *DON'T DO THIS:* 20 | 21 | ```clj 22 | (defprotocol CalculatorService 23 | (add [this x y])) 24 | 25 | (defservice calculator-service 26 | CalculatorService 27 | [] 28 | (add [this x y] (+ x y))) 29 | ``` 30 | 31 | This is better: 32 | 33 | ```clj 34 | (ns calculator.core) 35 | 36 | (defn add [x y] (+ x y)) 37 | ``` 38 | ```clj 39 | (ns calculator.service 40 | (:require calculator.core :as core)) 41 | 42 | (defprotocol CalculatorService 43 | (add [this x y])) 44 | 45 | (defservice calculator-service 46 | CalculatorService 47 | [] 48 | (add [this x y] (core/add x y))) 49 | ``` 50 | 51 | This way, you can test `calculator.core` directly, and re-use the functions it provides in other places without having to worry about Trapperkeeper. 52 | 53 | ## On Lifecycles 54 | 55 | 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: 56 | 57 | * 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'. 58 | * 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.) 59 | * 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. 60 | 61 | ## Testing Services 62 | 63 | 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. 64 | 65 | 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. 66 | -------------------------------------------------------------------------------- /documentation/Trapperkeeper-Quick-Start.md: -------------------------------------------------------------------------------- 1 | # Trapperkeeper Quick Start 2 | 3 | ## Lein Template 4 | 5 | A Leiningen template is available that shows a suggested project structure: 6 | 7 | lein new trapperkeeper my.namespace/myproject 8 | 9 | Once you've created a project from the template, you can run it via the lein alias: 10 | 11 | lein tk 12 | 13 | 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. 14 | 15 | ## Hello World 16 | 17 | Here's a "hello world" example for getting started with Trapperkeeper. 18 | 19 | First, you need to define one or more services: 20 | 21 | ```clj 22 | (ns hello 23 | (:require [puppetlabs.trapperkeeper.core :refer [defservice]])) 24 | 25 | ;; A protocol that defines what functions our service will provide 26 | (defprotocol HelloService 27 | (hello [this]) 28 | 29 | (defservice hello-service 30 | HelloService 31 | ;; dependencies: none for this service 32 | [] 33 | ;; optional lifecycle functions that we can implement if we choose 34 | (init [this context] 35 | (println "Hello service initializing!") 36 | context) 37 | ;; implement our protocol functions 38 | (hello [this] (println "Hello there!"))) 39 | 40 | (defservice hello-consumer-service 41 | ;; no protocol required since this service doesn't export any functions. 42 | ;; express a dependency on the `hello` function from the `HelloService`. 43 | [[:HelloService hello]] 44 | (init [this context] 45 | (println "Hello consumer initializing; hello service says:") 46 | ;; call the function from the `hello-service`! 47 | (hello) 48 | context)) 49 | ``` 50 | 51 | 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). 52 | 53 | ```clj 54 | hello/hello-consumer-service 55 | hello/hello-service 56 | ``` 57 | 58 | Lastly, set Trapperkeeper to be your `:main` in your Leiningen project file: 59 | 60 | ```clj 61 | :main puppetlabs.trapperkeeper.main 62 | ``` 63 | 64 | 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. 65 | -------------------------------------------------------------------------------- /examples/java_service/README.md: -------------------------------------------------------------------------------- 1 | Example: Building a Trapperkeeper service that wraps java code 2 | -------------------------------------------------------------- 3 | To run the example: 4 | 5 | lein trampoline run --config ./examples/java_service/config.conf \ 6 | --bootstrap-config ./examples/java_service/bootstrap.cfg 7 | -------------------------------------------------------------------------------- /examples/java_service/bootstrap.cfg: -------------------------------------------------------------------------------- 1 | java-service-example.java-service/java-service 2 | java-service-example.java-service/java-service-consumer -------------------------------------------------------------------------------- /examples/java_service/config.conf: -------------------------------------------------------------------------------- 1 | global { 2 | # Points to a logback config file 3 | logging-config = examples/java_service/logback.xml 4 | } -------------------------------------------------------------------------------- /examples/java_service/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d %-5p [%c{2}] %m%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/java_service/src/clj/java_service_example/java_service.clj: -------------------------------------------------------------------------------- 1 | (ns java-service-example.java-service 2 | (:import (java_service_example ServiceImpl)) 3 | (:require [puppetlabs.trapperkeeper.core :refer [defservice]] 4 | [clojure.tools.logging :as log])) 5 | 6 | (defprotocol JavaService 7 | (msg-fn [this]) 8 | (meaning-of-life-fn [this])) 9 | 10 | (defservice java-service 11 | JavaService 12 | [] 13 | ;; Service functions are implemented in a java `ServiceImpl` class 14 | (msg-fn [this] (ServiceImpl/getMessage)) 15 | (meaning-of-life-fn [this] (ServiceImpl/getMeaningOfLife))) 16 | 17 | (defservice java-service-consumer 18 | [[:JavaService msg-fn meaning-of-life-fn] 19 | [:ShutdownService request-shutdown]] 20 | (init [this context] 21 | (log/info "Java service consumer!") 22 | (log/infof "The message from Java is: '%s'" (msg-fn)) 23 | (log/infof "The meaning of life is: '%s'" (meaning-of-life-fn)) 24 | context) 25 | (start [this context] 26 | (request-shutdown) 27 | context)) 28 | -------------------------------------------------------------------------------- /examples/java_service/src/java/java_service_example/ServiceImpl.java: -------------------------------------------------------------------------------- 1 | package java_service_example; 2 | 3 | public class ServiceImpl { 4 | public static String getMessage() { return "This came from java."; } 5 | public static int getMeaningOfLife() { return 42; } 6 | } -------------------------------------------------------------------------------- /examples/shutdown_app/README.md: -------------------------------------------------------------------------------- 1 | This simple standalone application is for testing the shutdown functionality 2 | of Trapperkeeper. This is intended to be ran, and then killed with either 3 | Ctrl-C or the kill command, and the services with shutdown hooks should be 4 | called. 5 | 6 | You should see instructions upon starting the application. 7 | 8 | To run: 9 | lein test-external-shutdown 10 | -------------------------------------------------------------------------------- /examples/shutdown_app/bootstrap.cfg: -------------------------------------------------------------------------------- 1 | examples.shutdown-app.test-external-shutdown/test-service 2 | -------------------------------------------------------------------------------- /examples/shutdown_app/src/examples/shutdown_app/test_external_shutdown.clj: -------------------------------------------------------------------------------- 1 | (ns examples.shutdown-app.test-external-shutdown 2 | (:require [puppetlabs.trapperkeeper.core :as trapperkeeper] 3 | [puppetlabs.trapperkeeper.testutils.bootstrap :as testutils])) 4 | 5 | (trapperkeeper/defservice test-service 6 | [] 7 | (stop [this context] 8 | (println "If you see this printed out then shutdown works correctly!") 9 | context)) 10 | 11 | (defn -main 12 | [& args] 13 | (println "Waiting for a shutdown signal - use Ctrl-C or kill.") 14 | (println "You should see a message printed out when services are being shutdown.") 15 | (trapperkeeper/run 16 | {:config testutils/empty-config 17 | :bootstrap-config "examples/shutdown_app/bootstrap.cfg"})) 18 | -------------------------------------------------------------------------------- /ext/test/custom-exit-behavior: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -uexo pipefail 4 | 5 | usage() { echo "Usage: $(basename "$0")"; } 6 | misuse() { usage 1>&2; exit 2; } 7 | 8 | test $# -eq 0 || misuse 9 | 10 | tmpdir="$(mktemp -d "test-custom-exit-behavior-XXXXXX")" 11 | tmpdir="$(cd "$tmpdir" && pwd)" 12 | trap "$(printf 'rm -rf %q' "$tmpdir")" EXIT 13 | 14 | rc=0 15 | ./tk -cp "$(pwd)/test" -- \ 16 | -d -b <(echo puppetlabs.trapperkeeper.custom-exit-behavior-test/custom-exit-behavior-test-service) \ 17 | 1>"$tmpdir/out" 2>"$tmpdir/err" || rc=$? 18 | cat "$tmpdir/out" "$tmpdir/err" 19 | test "$rc" -eq 7 20 | grep -F 'Some excitement!' "$tmpdir/out" 21 | grep -F 'More excitement!' "$tmpdir/err" 22 | -------------------------------------------------------------------------------- /ext/test/run-all: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -uexo pipefail 4 | 5 | usage() { echo "Usage: [TRAPPERKEEPER_JAR=JAR] $(basename "$0")"; } 6 | misuse() { usage 1>&2; exit 2; } 7 | 8 | test $# -eq 0 || misuse 9 | 10 | ext/test/top-level-cli 11 | ext/test/custom-exit-behavior 12 | ext/test/signal-handling 13 | -------------------------------------------------------------------------------- /ext/test/signal-handling: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -uexo pipefail 4 | 5 | usage() { echo "Usage: $(basename "$0")"; } 6 | misuse() { usage 1>&2; exit 2; } 7 | 8 | await-file() 9 | ( 10 | local target="$1" 11 | set +x 12 | while ! test -e "$target"; do sleep 0.1; done 13 | ) 14 | 15 | tk_pid='' 16 | tmpdir='' 17 | on-exit() 18 | { 19 | if test "$tk_pid"; then 20 | kill "$tk_pid" 21 | status=0 22 | wait "$tk_pid" || status=$? 23 | set +x 24 | echo tk exited with status "$status (143 is likely)" 1>&2 25 | set -x 26 | fi 27 | rm -rf "$tmpdir" 28 | } 29 | trap on-exit EXIT 30 | 31 | test $# -eq 0 || misuse 32 | 33 | tmpdir="$(mktemp -d "test-signal-handling-XXXXXX")" 34 | tmpdir="$(cd "$tmpdir" && pwd)" 35 | 36 | # Start the test server, which repeatedly writes to the configured 37 | # target file, and make sure the target changes after a config file 38 | # change and signal. 39 | 40 | target_1="$tmpdir/target-1" 41 | target_2="$tmpdir/target-2" 42 | 43 | cat > "$tmpdir/config.json" < "$tmpdir/config.json" <&2; exit 2; } 7 | 8 | test $# -eq 0 || misuse 9 | 10 | tmpdir="$(mktemp -d "test-top-level-cli-XXXXXX")" 11 | tmpdir="$(cd "$tmpdir" && pwd)" 12 | trap "$(printf 'rm -rf %q' "$tmpdir")" EXIT 13 | 14 | 15 | ## Test handling an unknown option 16 | rc=0 17 | ./tk -- --invalid-option 1>"$tmpdir/out" 2>"$tmpdir/err" || rc=$? 18 | cat "$tmpdir/out" "$tmpdir/err" 19 | test "$rc" -eq 1 20 | grep -F 'Unknown option: "--invalid-option"' "$tmpdir/err" 21 | 22 | 23 | ## Test --help 24 | rc=0 25 | ./tk -- --help 1>"$tmpdir/out" 2>"$tmpdir/err" || rc=$? 26 | cat "$tmpdir/out" "$tmpdir/err" 27 | test "$rc" -eq 0 28 | grep -F 'Path to bootstrap config file' "$tmpdir/out" 29 | test $(grep -c -F 'Path to bootstrap config file' "$tmpdir/out") -eq 1 30 | test $(wc -c < "$tmpdir/out") -eq 650 31 | 32 | 33 | ## Test handling a missing bootstrap file 34 | rc=0 35 | ./tk -- frobnicate ... 1>"$tmpdir/out" 2>"$tmpdir/err" || rc=$? 36 | cat "$tmpdir/out" "$tmpdir/err" 37 | test "$rc" -eq 1 38 | grep -F 'Unable to find bootstrap.cfg file via --bootstrap-config' "$tmpdir/err" 39 | -------------------------------------------------------------------------------- /ext/travisci/prep-macos: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -exu 4 | 5 | java -version 6 | 7 | # Something was wrong with travis' macos lein, so just grab our own 8 | mkdir -p ext/travisci/bin 9 | curl -o lein 'https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein' 10 | chmod +x lein 11 | mv lein ext/travisci/bin/ 12 | -------------------------------------------------------------------------------- /jenkins/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -x 5 | 6 | git fetch --tags 7 | 8 | lein test 9 | echo "Tests passed!" 10 | 11 | lein release 12 | echo "Release plugin successful, pushing changes to git" 13 | 14 | git push origin --tags HEAD:$TRAPPERKEEPER_BRANCH 15 | 16 | echo "git push successful." 17 | -------------------------------------------------------------------------------- /plugin-test-resources/bad-plugins/kitchensink-0.1.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puppetlabs/trapperkeeper/f7772765f17d3a0bc0d8af022d811eaa6cc0c539/plugin-test-resources/bad-plugins/kitchensink-0.1.0.jar -------------------------------------------------------------------------------- /plugin-test-resources/plugins/test-service.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puppetlabs/trapperkeeper/f7772765f17d3a0bc0d8af022d811eaa6cc0c539/plugin-test-resources/plugins/test-service.jar -------------------------------------------------------------------------------- /plugin-test-resources/src/test_services/plugin_test_services.clj: -------------------------------------------------------------------------------- 1 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 2 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 3 | ;; 4 | ;; IMPORTANT 5 | ;; 6 | ;; If you change this file, you need to run the following command to update the 7 | ;; .jar generated from it (for testing plugins): 8 | ;; 9 | ;; zip -r ../plugins/test-service.jar test_services 10 | ;; 11 | ;; This requires that your cwd is 12 | ;; plugin-test-resources/src 13 | ;; 14 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 15 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 16 | 17 | (ns test-services.plugin-test-services 18 | (:require [puppetlabs.trapperkeeper.core :refer [defservice]])) 19 | 20 | (defprotocol PluginTestService 21 | (moo [this])) 22 | 23 | (defservice plugin-test-service 24 | PluginTestService 25 | [] 26 | (moo [this] "This message comes from the plugin test service.")) 27 | 28 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject puppetlabs/trapperkeeper "4.0.3-SNAPSHOT" 2 | :description "A framework for configuring, composing, and running Clojure services." 3 | 4 | :license {:name "Apache License, Version 2.0" 5 | :url "http://www.apache.org/licenses/LICENSE-2.0.html"} 6 | 7 | :min-lein-version "2.9.0" 8 | 9 | :parent-project {:coords [puppetlabs/clj-parent "6.0.1"] 10 | :inherit [:managed-dependencies]} 11 | 12 | ;; Abort when version ranges or version conflicts are detected in 13 | ;; dependencies. Also supports :warn to simply emit warnings. 14 | ;; requires lein 2.2.0+. 15 | :pedantic? :abort 16 | :dependencies [[org.clojure/clojure] 17 | [org.clojure/tools.logging] 18 | [org.clojure/tools.macro] 19 | [org.clojure/core.async] 20 | 21 | [org.slf4j/slf4j-api] 22 | [org.slf4j/log4j-over-slf4j] 23 | [ch.qos.logback/logback-classic] 24 | ;; even though we don't strictly have a dependency on the following two 25 | ;; logback artifacts, specifying the dependency version here ensures 26 | ;; that downstream projects don't pick up different versions that would 27 | ;; conflict with our version of logback-classic 28 | [ch.qos.logback/logback-core] 29 | [ch.qos.logback/logback-access] 30 | ;; Janino can be used for some advanced logback configurations 31 | [org.codehaus.janino/janino] 32 | 33 | [clj-time] 34 | [clj-commons/fs] 35 | 36 | [prismatic/plumbing] 37 | [prismatic/schema] 38 | 39 | [beckon] 40 | 41 | [puppetlabs/typesafe-config] 42 | ;; exclusion added due to dependency conflict over asm and jackson-dataformat-cbor 43 | ;; see https://github.com/puppetlabs/trapperkeeper/pull/306#issuecomment-1467059264 44 | [puppetlabs/kitchensink nil :exclusions [cheshire]] 45 | [puppetlabs/i18n] 46 | [nrepl/nrepl] 47 | [io.github.clj-kondo/config-slingshot-slingshot "1.0.0"]] 48 | 49 | :deploy-repositories [["releases" {:url "https://clojars.org/repo" 50 | :username :env/clojars_jenkins_username 51 | :password :env/clojars_jenkins_password 52 | :sign-releases false}]] 53 | 54 | ;; Convenience for manually testing application shutdown support - run `lein test-external-shutdown` 55 | :aliases {"test-external-shutdown" ["trampoline" "run" "-m" "examples.shutdown-app.test-external-shutdown"]} 56 | 57 | ;; By declaring a classifier here and a corresponding profile below we'll get an additional jar 58 | ;; during `lein jar` that has all the code in the test/ directory. Downstream projects can then 59 | ;; depend on this test jar using a :classifier in their :dependencies to reuse the test utility 60 | ;; code that we have. 61 | :classifiers [["test" :testutils]] 62 | 63 | :profiles {:dev {:source-paths ["examples/shutdown_app/src" 64 | "examples/java_service/src/clj"] 65 | :java-source-paths ["examples/java_service/src/java"] 66 | :dependencies [[puppetlabs/kitchensink nil :classifier "test" :exclusions [cheshire]]]} 67 | 68 | :testutils {:source-paths ^:replace ["test"]} 69 | :uberjar {:aot [puppetlabs.trapperkeeper.main] 70 | :classifiers ^:replace []}} 71 | 72 | :plugins [[lein-parent "0.3.7"] 73 | [jonase/eastwood "1.2.2" :exclusions [org.clojure/clojure]] 74 | [puppetlabs/i18n "0.9.2"]] 75 | 76 | :eastwood {:ignored-faults {:reflection {puppetlabs.trapperkeeper.logging [{:line 92}] 77 | puppetlabs.trapperkeeper.internal [{:line 128}] 78 | puppetlabs.trapperkeeper.testutils.logging true 79 | puppetlabs.trapperkeeper.testutils.logging-test true 80 | puppetlabs.trapperkeeper.services.nrepl.nrepl-service-test true 81 | puppetlabs.trapperkeeper.plugins-test true} 82 | :local-shadows-var {puppetlabs.trapperkeeper.config-test true 83 | puppetlabs.trapperkeeper.services-test true 84 | java-service-example.java-service true 85 | puppetlabs.trapperkeeper.optional-deps-test true} 86 | :deprecations {puppetlabs.trapperkeeper.testutils.logging true 87 | puppetlabs.trapperkeeper.testutils.logging-test true 88 | puppetlabs.trapperkeeper.logging-test true} 89 | :def-in-def {puppetlabs.trapperkeeper.optional-deps-test true}} 90 | 91 | :continue-on-exception true} 92 | 93 | :main puppetlabs.trapperkeeper.main) 94 | 95 | -------------------------------------------------------------------------------- /src/puppetlabs/trapperkeeper/app.clj: -------------------------------------------------------------------------------- 1 | (ns puppetlabs.trapperkeeper.app 2 | (:require [schema.core :as schema] 3 | [puppetlabs.trapperkeeper.services :as s] 4 | [clojure.core.async.impl.protocols :as async-prot]) 5 | (:import (clojure.lang IDeref))) 6 | 7 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 8 | ;;; Schema 9 | 10 | (def TrapperkeeperAppOrderedServices 11 | [[(schema/one schema/Keyword "service-id") 12 | (schema/one (schema/protocol s/Service) "Service")]]) 13 | 14 | (def TrapperkeeperAppContext 15 | "Schema for a Trapperkeeper application's internal context. NOTE: this schema 16 | is intended for internal use by TK and may be subject to minor changes in future 17 | releases." 18 | {:service-contexts {schema/Keyword {schema/Any schema/Any}} 19 | :ordered-services TrapperkeeperAppOrderedServices 20 | :services-by-id {schema/Keyword (schema/protocol s/Service)} 21 | :lifecycle-channel (schema/protocol async-prot/Channel) 22 | :shutdown-channel (schema/protocol async-prot/Channel) 23 | :lifecycle-worker (schema/protocol async-prot/Channel) 24 | :shutdown-reason-promise IDeref}) 25 | 26 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 27 | ;;; App Protocol 28 | 29 | (defprotocol TrapperkeeperApp 30 | "Functions available on a trapperkeeper application instance" 31 | (get-service [this service-id] "Returns the service with the given service id") 32 | (service-graph [this] "Returns the prismatic graph of service fns for this app") 33 | (app-context [this] "Returns the application context for this app (an atom containing a map)") 34 | (check-for-errors! [this] (str "Check for any errors which have occurred in " 35 | "the bootstrap process. If any have " 36 | "occurred, throw a `java.lang.Throwable` with " 37 | "the contents of the error. If none have " 38 | "occurred, return the input parameter.")) 39 | (init [this] "Initialize the services") 40 | (start [this] "Start the services") 41 | (stop [this] [this throw?] "Stop the services") 42 | (restart [this] "Stop and restart the services")) 43 | -------------------------------------------------------------------------------- /src/puppetlabs/trapperkeeper/common.clj: -------------------------------------------------------------------------------- 1 | (ns puppetlabs.trapperkeeper.common 2 | (:require [schema.core :as schema])) 3 | 4 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 5 | ;; Schemas 6 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 7 | 8 | (def CLIData {(schema/optional-key :debug) schema/Bool 9 | (schema/optional-key :bootstrap-config) schema/Str 10 | (schema/optional-key :config) schema/Str 11 | (schema/optional-key :plugins) schema/Str 12 | (schema/optional-key :restart-file) schema/Str 13 | (schema/optional-key :help) schema/Bool}) 14 | -------------------------------------------------------------------------------- /src/puppetlabs/trapperkeeper/config.clj: -------------------------------------------------------------------------------- 1 | ;;;; 2 | ;;;; This namespace contains trapperkeeper's built-in configuration service, 3 | ;;;; which is based on .ini config files. 4 | ;;;; 5 | ;;;; This service provides a function, `get-in-config`, which can be used to 6 | ;;;; retrieve the config data read from the ini files. For example, 7 | ;;;; given an .ini file with the following contents: 8 | ;;;; 9 | ;;;; [foo] 10 | ;;;; bar = baz 11 | ;;;; 12 | ;;;; The value of `(get-in-config [:foo :bar])` would be `"baz"`. 13 | ;;;; 14 | ;;;; Also provides a second function, `get-config`, which simply returns 15 | ;;;; the entire map of configuration data. 16 | ;;;; 17 | 18 | (ns puppetlabs.trapperkeeper.config 19 | (:import (java.io FileNotFoundException PushbackReader)) 20 | (:require [clojure.java.io :as io] 21 | [clojure.string :as str] 22 | [clojure.edn :as edn] 23 | [me.raynes.fs :as fs] 24 | [puppetlabs.kitchensink.core :as ks] 25 | [puppetlabs.config.typesafe :as typesafe] 26 | [puppetlabs.trapperkeeper.services :refer [service service-context]] 27 | [puppetlabs.trapperkeeper.logging :refer [configure-logging!]] 28 | [clojure.tools.logging :as log] 29 | [schema.core :as schema] 30 | [puppetlabs.trapperkeeper.common :as common] 31 | [puppetlabs.i18n.core :as i18n])) 32 | 33 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 34 | ;;; Service protocol 35 | 36 | (defprotocol ConfigService 37 | (get-config [this] "Returns a map containing all of the configuration values") 38 | (get-in-config [this ks] [this ks default] 39 | "Returns the individual configuration value from the nested 40 | configuration structure, where ks is a sequence of keys. 41 | Returns nil if the key is not present, or the default value if 42 | supplied.")) 43 | 44 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 45 | ;;; Private 46 | 47 | (defn config-file->map 48 | [file] 49 | (condp (fn [vals ext] (contains? vals ext)) (fs/extension file) 50 | #{".ini"} 51 | (ks/ini-to-map file) 52 | 53 | #{".json" ".conf" ".properties"} 54 | (typesafe/config-file->map file) 55 | 56 | #{".edn"} 57 | (edn/read (PushbackReader. (io/reader file))) 58 | 59 | (throw (IllegalArgumentException. 60 | (i18n/trs "Config file {0} must end in .conf or other recognized extension" 61 | (-> file str pr-str)))))) 62 | 63 | (defn override-restart-file-from-cli-data 64 | [config-data cli-data] 65 | (if-let [cli-restart-file (:restart-file cli-data)] 66 | (do 67 | (when (get-in config-data [:global :restart-file]) 68 | (log/warnf (i18n/trs "restart-file setting specified both on command-line and in config file, using command-line value: ''{0}''" 69 | cli-restart-file))) 70 | (assoc-in config-data [:global :restart-file] cli-restart-file)) 71 | config-data)) 72 | 73 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 74 | ;;; Public 75 | 76 | (defn get-files-from-config 77 | "Given a path to a file or directory, return a list of all files 78 | contained that have valid extensions for a TK config." 79 | [path] 80 | (when-not (.canRead (io/file path)) 81 | (throw (FileNotFoundException. 82 | (i18n/trs "Configuration path ''{0}'' must exist and must be readable." 83 | path)))) 84 | (if-not (fs/directory? path) 85 | [path] 86 | (mapcat 87 | #(fs/glob (fs/file path %)) 88 | ["*.ini" "*.conf" "*.json" "*.properties" "*.edn"]))) 89 | 90 | (defn load-config 91 | "Given a path to a configuration file or directory of configuration files, 92 | or a string of multiple paths separated by comma, parse the config files and build 93 | up a trapperkeeper config map. Can be used to implement CLI tools that need 94 | access to trapperkeeper config data but don't need to boot the full TK framework." 95 | [paths] 96 | (let [files (flatten (map get-files-from-config (str/split paths #",")))] 97 | (->> files 98 | (map ks/absolute-path) 99 | (map config-file->map) 100 | (apply ks/deep-merge-with-keys 101 | (fn [ks & _] 102 | (throw (IllegalArgumentException. 103 | (i18n/trs "Duplicate configuration entry: {0}" ks))))) 104 | (merge {})))) 105 | 106 | (defn config-service 107 | "Returns trapperkeeper's configuration service. Expects 108 | to find a command-line argument value for `:config`; the value of this 109 | parameter should be the path to an .ini file or a directory of .ini files." 110 | [config-data-fn] 111 | (service ConfigService 112 | [] 113 | (init [this context] 114 | (assoc context :config (config-data-fn))) 115 | (get-config [this] 116 | (let [{:keys [config]} (service-context this)] 117 | config)) 118 | (get-in-config [this ks] 119 | (let [{:keys [config]} (service-context this)] 120 | (get-in config ks))) 121 | (get-in-config [this ks default] 122 | (let [{:keys [config]} (service-context this)] 123 | (get-in config ks default))))) 124 | 125 | (schema/defn parse-config-data :- (schema/pred map?) 126 | "Parses the .ini, .edn, .conf, .json, or .properties configuration file(s) 127 | and returns a map of configuration data. If no configuration file is 128 | explicitly specified, will act as if it was given an empty configuration 129 | file." 130 | [cli-data :- common/CLIData] 131 | (let [debug? (or (:debug cli-data) false)] 132 | (if-not (contains? cli-data :config) 133 | {:debug debug?} 134 | (-> (:config cli-data) 135 | (load-config) 136 | (assoc :debug debug?) 137 | (override-restart-file-from-cli-data cli-data))))) 138 | 139 | (defn initialize-logging! 140 | "Initializes the logging system based on the configuration data." 141 | [config-data] 142 | (let [debug? (get-in config-data [:debug]) 143 | log-config (get-in config-data [:global :logging-config])] 144 | (configure-logging! log-config debug?))) 145 | -------------------------------------------------------------------------------- /src/puppetlabs/trapperkeeper/logging.clj: -------------------------------------------------------------------------------- 1 | (ns puppetlabs.trapperkeeper.logging 2 | (:import [ch.qos.logback.classic Level LoggerContext PatternLayout] 3 | (ch.qos.logback.core ConsoleAppender) 4 | (org.slf4j Logger LoggerFactory) 5 | (ch.qos.logback.classic.joran JoranConfigurator)) 6 | (:require [clojure.stacktrace :refer [print-cause-trace]] 7 | [clojure.tools.logging :as log] 8 | [puppetlabs.i18n.core :as i18n])) 9 | 10 | (defn logging-context 11 | ^LoggerContext [] 12 | ;; in practice, this returns ch.qos.logback.classic.LoggerContext 13 | ;; which the other functions below assume 14 | (LoggerFactory/getILoggerFactory)) 15 | 16 | (defn reset-logging 17 | [] 18 | (.reset (logging-context))) 19 | 20 | (def root-logger-name Logger/ROOT_LOGGER_NAME) 21 | 22 | (defn root-logger 23 | ^ch.qos.logback.classic.Logger [] 24 | (LoggerFactory/getLogger ^String root-logger-name)) 25 | 26 | (defn catch-all-logger 27 | "A logging function useful for catch-all purposes, that is, to 28 | ensure that a log message gets in front of a user the best we can 29 | even if that means duplicated output. 30 | 31 | This is really only suitable for _last-ditch_ exception handling, 32 | where we want to make sure an exception is logged (because nobody 33 | higher up in the stack will log it for us)." 34 | ([exception] 35 | (catch-all-logger exception (i18n/trs "Uncaught exception"))) 36 | ([exception message] 37 | (print-cause-trace exception) 38 | (flush) 39 | (log/error exception message))) 40 | 41 | (defn create-console-appender 42 | "Instantiates and returns a logging appender configured to write to 43 | the console, using the standard logging configuration. 44 | 45 | `level` is an optional argument (of type `org.apache.log4j.Level`) 46 | indicating the logging threshold for the new appender. Defaults 47 | to `DEBUG`." 48 | ([] 49 | (create-console-appender Level/DEBUG)) 50 | ([level] 51 | {:pre [(instance? Level level)]} 52 | (let [layout (PatternLayout.)] 53 | (doto layout 54 | (.setContext (logging-context)) 55 | (.setPattern "%d %-5p [%t] [%c{2}] %m%n") 56 | (.start)) 57 | (doto (ConsoleAppender.) 58 | (.setContext (logging-context)) 59 | (.setLayout layout) 60 | (.start))))) 61 | 62 | (defn add-console-logger! 63 | "Adds a console logger to the current logging configuration, and ensures 64 | that the root logger is set to log at the logging level of the new 65 | logger or finer. 66 | 67 | `level` is an optional argument (of type `org.apache.log4j.Level`) 68 | indicating the logging threshold for the new logger. Defaults 69 | to `DEBUG`." 70 | ([] 71 | (add-console-logger! Level/DEBUG)) 72 | ([level] 73 | {:pre [(instance? Level level)]} 74 | (let [root (root-logger)] 75 | (.addAppender root (create-console-appender level)) 76 | (when (> (.toInt (.getLevel root)) 77 | (.toInt ^Level level)) 78 | (.setLevel root level))))) 79 | 80 | (defn configure-logger! 81 | "Reconfigures the current logger based on the supplied configuration. 82 | 83 | Supplied configuration can be a file path, url, file, InputStream, or 84 | InputSource. It is passed along unchanged to `doConfigure` for 85 | JoranConfigurator. For more information, see the documentation for 86 | ch.qos.logback.core.classic.joran.JoranConfigurator." 87 | [logging-conf] 88 | (let [configurator (JoranConfigurator.) 89 | context (logging-context)] 90 | (.setContext configurator context) 91 | (.reset context) 92 | (.doConfigure configurator logging-conf))) 93 | 94 | (defn configure-logging! 95 | "Takes a file path, url, file, InputStream, or InputSource which can 96 | define how to configure the logging system. This is passed unchanged 97 | to the `doConfigure` method for the underlying JoranConfigurator 98 | class. 99 | 100 | Also takes an optional `debug` flag which turns on debug logging." 101 | ([logging-conf] 102 | (configure-logging! logging-conf false)) 103 | ([logging-conf debug] 104 | (when logging-conf 105 | (configure-logger! logging-conf)) 106 | (when debug 107 | (add-console-logger! Level/DEBUG) 108 | (log/debug (i18n/trs "Debug logging enabled"))))) 109 | -------------------------------------------------------------------------------- /src/puppetlabs/trapperkeeper/main.clj: -------------------------------------------------------------------------------- 1 | (ns puppetlabs.trapperkeeper.main 2 | (:gen-class)) 3 | 4 | (defn -main 5 | [& args] 6 | (require 'puppetlabs.trapperkeeper.core) 7 | (apply (resolve 'puppetlabs.trapperkeeper.core/main) args)) 8 | 9 | -------------------------------------------------------------------------------- /src/puppetlabs/trapperkeeper/plugins.clj: -------------------------------------------------------------------------------- 1 | (ns puppetlabs.trapperkeeper.plugins 2 | (:import (java.util.jar JarEntry JarFile) 3 | (java.io File)) 4 | (:require [clojure.java.io :refer [file]] 5 | [clojure.tools.logging :as log] 6 | [puppetlabs.kitchensink.classpath :as kitchensink] 7 | [puppetlabs.i18n.core :as i18n])) 8 | 9 | (defn- should-process? 10 | "Helper for `process-file`. Answers whether or not the duplicate detection 11 | code should process a file with the given name." 12 | [^String name] 13 | (and 14 | ;; ignore directories 15 | (not (or (.isDirectory (file name)) 16 | (.endsWith name "/"))) ; necessary for directories in .jars 17 | 18 | ;; don't care about anything in META-INF 19 | (not (.startsWith name "META-INF")) 20 | 21 | ;; lein includes project.clj ... no thank you 22 | (not (= name "project.clj")))) 23 | 24 | (defn- handle-duplicate! 25 | "Helper for `process-file`; handles a found duplicate. Throws an exception 26 | if the duplicate is a .class or .clj file. Otherwise, logs a warning and 27 | returns the accumulator." 28 | [container-filename acc ^String filename] 29 | (let [error-msg (i18n/trs "Class or namespace {0} found in both {1} and {2}" 30 | filename container-filename (acc filename))] 31 | (if (or (.endsWith filename ".class") (.endsWith filename ".clj")) 32 | (throw (IllegalArgumentException. ^String error-msg)) 33 | 34 | ;; It is common to have other conflicts (besides classes and clojure 35 | ;; namespaces), especially during development (for example, 36 | ;; jetty-servlet and jetty-http both contain an `about.html` - 37 | ;; these conflicts don't exist in the uberjar anyway, 38 | ;; and likely aren't important. 39 | (log/warn error-msg))) 40 | acc) 41 | 42 | (defn- process-file 43 | "Helper for `process-container`. Processes a file and adds it to the 44 | accumulator if it is a .class or .clj file we care about." 45 | [container-filename acc filename] 46 | (if (should-process? filename) 47 | (if (contains? acc filename) 48 | (handle-duplicate! container-filename acc filename) 49 | (assoc acc filename container-filename)) 50 | acc)) 51 | 52 | (defn- process-container 53 | "Helper for `verify-no-duplicate-resources`. 54 | Processes a .jar file or directory that contains classes and/or .clj sources 55 | and builds up map of .class/.clj filenames -> container names." 56 | [acc container-filename] 57 | (let [file (file container-filename)] 58 | (if (.exists file) 59 | (let [filenames (if (.isDirectory file) 60 | (map #(.getPath ^File %) (file-seq file)) 61 | (map #(.getName ^JarEntry %) (enumeration-seq (.entries (JarFile. file)))))] 62 | (reduce (partial process-file container-filename) acc filenames)) 63 | acc))) ; There may be directories on the classpath that do not exist. 64 | 65 | 66 | (defn jars-in-dir 67 | "Given a path to a directory on disk, returns a collection of all of the .jar 68 | files contained in that directory (not recursive)." 69 | [^File dir] 70 | {:pre [(instance? File dir)] 71 | :post [(coll? %) 72 | (every? (partial instance? File) %)]} 73 | (filter #(.endsWith (.getAbsolutePath ^File %) ".jar") (.listFiles dir))) 74 | 75 | (defn verify-no-duplicate-resources 76 | "Examines all resources on the classpath and contained in the given directory 77 | and checks for duplicates. A resource in this context is defined as a .class 78 | or .clj file. Throws an Exception if any duplicates are found." 79 | [dir] 80 | {:pre [(instance? File dir)]} 81 | (let [plugin-jars (jars-in-dir dir) 82 | classpath (System/getProperty "java.class.path") 83 | ;; When running as an uberjar, this system property contains only 84 | ;; the path to the uberjar (-classpath is ignored). 85 | classpath-containers (if (.contains classpath ":") 86 | (.split classpath ":") 87 | [classpath]) 88 | all-containers (concat plugin-jars classpath-containers)] 89 | (reduce process-container {} all-containers))) 90 | 91 | (defn add-plugin-jars-to-classpath! 92 | "Add all of .jar files contained in the plugins directory 93 | (specified by the '--plugins' CLI argument) to the classpath." 94 | [plugins-path] 95 | (when plugins-path 96 | (let [plugins (file plugins-path)] 97 | (if (.exists plugins) 98 | (do 99 | (verify-no-duplicate-resources plugins) 100 | (doseq [^File jar (jars-in-dir plugins)] 101 | (log/info (i18n/trs "Adding plugin {0} to classpath." (.getAbsolutePath jar))) 102 | (kitchensink/add-classpath jar) 103 | (kitchensink/add-classpath jar (clojure.lang.RT/baseLoader)))) 104 | (let [^String msg (i18n/trs "Plugins directory {0} does not exist" plugins-path)] 105 | (throw (IllegalArgumentException. msg))))))) 106 | -------------------------------------------------------------------------------- /src/puppetlabs/trapperkeeper/services.clj: -------------------------------------------------------------------------------- 1 | (ns puppetlabs.trapperkeeper.services 2 | (:require [plumbing.core :refer [fnk]] 3 | [puppetlabs.trapperkeeper.services-internal :as si] 4 | [schema.core :as schema] 5 | [puppetlabs.i18n.core :as i18n])) 6 | 7 | (defprotocol Lifecycle 8 | "Lifecycle functions for a service. All services satisfy this protocol, and 9 | the lifecycle functions for each service will be called at the appropriate 10 | phase during the application lifecycle." 11 | (init [this context] "Initialize the service, given a context map. 12 | Must return the (possibly modified) context map.") 13 | (start [this context] "Start the service, given a context map. 14 | Must return the (possibly modified) context map.") 15 | (stop [this context] "Stop the service, given a context map. 16 | Must return the (possibly modified) context map.")) 17 | 18 | (defprotocol Service 19 | "Common functions available to all services" 20 | (service-id [this] "An identifier for the service") 21 | (service-context [this] "Returns the context map for this service") 22 | (get-service [this service-id] "Returns the service with the given service id. Throws if service not present") 23 | (maybe-get-service [this service-id] "Returns the service with the given service id. Returns nil if service not present") 24 | (get-services [this] "Returns a sequence containing all of the services in the app") 25 | (service-included? [this service-id] "Returns true or false whether service is included") 26 | (service-symbol [this] "The namespaced symbol of the service definition, or `nil` 27 | if no service symbol was provided.")) 28 | 29 | (defprotocol ServiceDefinition 30 | "A service definition. This protocol is for internal use only. The service 31 | is not usable until it is instantiated (via `boot!`)." 32 | (service-def-id [this] "An identifier for the service") 33 | (service-map [this] "The map of service functions for the graph")) 34 | 35 | (def lifecycle-fn-names (map :name (vals (:sigs Lifecycle)))) 36 | 37 | (defn name-with-attributes 38 | "This is a plate of warm and nutritious copypasta of 39 | clojure.tools.macro/name-with-attributes. Without this modified version, 40 | name-with-attributes consumes a dependency map when a protocol is not present 41 | in a defservice invocation. This version of the function double checks a map 42 | that might be metadata and ignores it if it conforms to the DependencyMap 43 | schema. Forgive me." 44 | [name macro-args] 45 | (let [[docstring macro-args] (if (string? (first macro-args)) 46 | [(first macro-args) (next macro-args)] 47 | [nil macro-args]) 48 | [attr macro-args] (if (and (map? (first macro-args)) 49 | (schema/check si/DependencyMap (first macro-args))) 50 | [(first macro-args) (next macro-args)] 51 | [{} macro-args]) 52 | attr (if docstring 53 | (assoc attr :doc docstring) 54 | attr) 55 | attr (if (meta name) 56 | (conj (meta name) attr) 57 | attr)] 58 | [(with-meta name attr) macro-args])) 59 | 60 | (defmacro service 61 | "Create a Trapperkeeper ServiceDefinition. 62 | 63 | First argument (optional) is a protocol indicating the list of functions that 64 | this service exposes for use by other Trapperkeeper services. 65 | 66 | Second argument is the dependency list; this should be a vector of vectors. 67 | Each inner vector should begin with a keyword representation of the name of the 68 | service protocol that the service depends upon. All remaining items in the inner 69 | vectors should be symbols representing functions that should be imported from 70 | the service. 71 | 72 | The remaining arguments should be function definitions for this service, specified 73 | in the format that is used by a normal clojure `reify`. The legal list of functions 74 | that may be specified includes whatever functions are defined by this service's 75 | protocol (if it has one), plus the list of functions in the `Lifecycle` protocol." 76 | [& forms] 77 | (let [{:keys [service-sym service-protocol-sym service-id service-fn-map 78 | dependencies fns-map]} 79 | (si/parse-service-forms! 80 | lifecycle-fn-names 81 | forms) 82 | output-schema (si/build-output-schema (keys service-fn-map))] 83 | `(reify ServiceDefinition 84 | (service-def-id [this] ~service-id) 85 | ;; service map for prismatic graph 86 | (service-map [this] 87 | {~service-id 88 | ;; the main service fnk for the app graph. we add metadata to the fnk 89 | ;; arguments list to specify an explicit output schema for the fnk 90 | (fnk service-fnk# :- ~output-schema 91 | ~(conj dependencies 'tk-app-context 'tk-service-refs) 92 | (let [svc# (reify 93 | Service 94 | (service-id [this#] ~service-id) 95 | (service-context [this#] (get-in ~'@tk-app-context [:service-contexts ~service-id] {})) 96 | (get-service [this# service-id#] 97 | (or (get-in ~'@tk-app-context [:services-by-id service-id#]) 98 | (throw (IllegalArgumentException. 99 | (i18n/trs "Call to ''get-service'' failed; service ''{0}'' does not exist." 100 | service-id#))))) 101 | (maybe-get-service [this# service-id#] 102 | (get-in ~'@tk-app-context [:services-by-id service-id#] nil)) 103 | (get-services [this#] 104 | (-> ~'@tk-app-context 105 | :services-by-id 106 | (dissoc :ConfigService :ShutdownService) 107 | vals)) 108 | (service-symbol [this#] '~service-sym) 109 | (service-included? [this# service-id#] 110 | (not (nil? (get-in ~'@tk-app-context [:services-by-id service-id#] nil)))) 111 | 112 | Lifecycle 113 | ~@(si/fn-defs fns-map lifecycle-fn-names) 114 | 115 | ~@(when service-protocol-sym 116 | `(~service-protocol-sym 117 | ~@(si/fn-defs fns-map (vals service-fn-map)))))] 118 | (swap! ~'tk-service-refs assoc ~service-id svc#) 119 | (si/build-service-map ~service-fn-map svc#)))})))) 120 | 121 | (defmacro defservice 122 | [svc-name & forms] 123 | (let [service-sym (symbol (name (ns-name *ns*)) (name svc-name)) 124 | [svc-name forms] (name-with-attributes svc-name forms)] 125 | `(def ~svc-name (service {:service-symbol ~service-sym} ~@forms)))) 126 | 127 | -------------------------------------------------------------------------------- /src/puppetlabs/trapperkeeper/services/nrepl/nrepl_service.clj: -------------------------------------------------------------------------------- 1 | (ns puppetlabs.trapperkeeper.services.nrepl.nrepl-service 2 | (:require 3 | [clojure.tools.logging :as log] 4 | [nrepl.server :as nrepl] 5 | [puppetlabs.kitchensink.core :refer [to-bool]] 6 | [puppetlabs.trapperkeeper.core :refer [defservice]] 7 | [puppetlabs.i18n.core :as i18n])) 8 | 9 | 10 | ;; If no port is specified in the config then 7888 is used 11 | (def ^{:private true} default-nrepl-port 7888) 12 | (def ^{:private true} default-bind-addr "0.0.0.0") 13 | (def ^{:private true} default-middlewares []) 14 | 15 | (defn- parse-middlewares-if-necessary 16 | [middlewares] 17 | (if (string? middlewares) 18 | (read-string middlewares) 19 | (map symbol middlewares))) 20 | 21 | (defn- process-middlewares [middlewares] 22 | (let [middlewares (parse-middlewares-if-necessary middlewares)] 23 | (doseq [middleware (map #(symbol (namespace %)) middlewares)] 24 | (require middleware)) 25 | (let [resolved (map #(resolve %) middlewares)] 26 | (apply nrepl/default-handler resolved)))) 27 | 28 | (defn process-config 29 | [get-in-config] 30 | {:enabled? (to-bool (get-in-config [:nrepl :enabled])) 31 | :port (get-in-config [:nrepl :port] default-nrepl-port) 32 | :bind (get-in-config [:nrepl :host] default-bind-addr) 33 | :handler (process-middlewares (get-in-config [:nrepl :middlewares] default-middlewares))}) 34 | 35 | (defn- startup-nrepl 36 | [get-in-config] 37 | (let [{:keys [enabled? port bind handler]} (process-config get-in-config)] 38 | (if enabled? 39 | (do (log/info (i18n/trs "Starting nREPL service on {0} port {1}" bind port)) 40 | (nrepl/start-server :port port :bind bind :handler handler)) 41 | (log/info (i18n/trs "nREPL service disabled, not starting"))))) 42 | 43 | (defn- shutdown-nrepl 44 | [nrepl-server] 45 | (when nrepl-server 46 | (log/info (i18n/trs "Shutting down nREPL service")) 47 | (nrepl/stop-server nrepl-server))) 48 | 49 | (defservice nrepl-service 50 | "The nREPL trapperkeeper service starts up a Clojure network REPL (nREPL) server attached to the running 51 | trapperkeeper process. It is configured in the following manner: 52 | 53 | [nrepl] 54 | enabled=true 55 | port=7888 56 | host=0.0.0.0 57 | 58 | The nrepl service will only start if enabled is set to true, and the port specified which port nREPL should bind to. 59 | If no port is specified then the default port of 7888 is used." 60 | [[:ConfigService get-in-config]] 61 | (init [this context] 62 | (let [nrepl-server (startup-nrepl get-in-config)] 63 | (assoc context :nrepl-server nrepl-server))) 64 | (stop [this context] 65 | (shutdown-nrepl (context :nrepl-server)) 66 | context)) 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /test/puppetlabs/trapperkeeper/config_test.clj: -------------------------------------------------------------------------------- 1 | (ns puppetlabs.trapperkeeper.config-test 2 | (:import (java.io FileNotFoundException)) 3 | (:require [clojure.test :refer :all] 4 | [puppetlabs.trapperkeeper.testutils.bootstrap :refer [bootstrap-services-with-cli-data with-app-with-cli-data]] 5 | [puppetlabs.trapperkeeper.app :refer [get-service]] 6 | [puppetlabs.trapperkeeper.services :refer [defservice]] 7 | [puppetlabs.trapperkeeper.config :refer [load-config]] 8 | [schema.test :as schema-test])) 9 | 10 | (use-fixtures :once schema-test/validate-schemas) 11 | 12 | (defprotocol ConfigTestService 13 | (test-fn [this ks]) 14 | (test-fn2 [this]) 15 | (get-in-config [this ks] [this ks default])) 16 | 17 | (defservice test-service 18 | ConfigTestService 19 | [[:ConfigService get-in-config get-config]] 20 | (test-fn [this ks] (get-in-config ks)) 21 | (test-fn2 [this] (get-config)) 22 | (get-in-config [this ks] (get-in-config ks)) 23 | (get-in-config [this ks default] (get-in-config ks default))) 24 | 25 | (deftest test-config-service 26 | (testing "Fails if config path doesn't exist" 27 | (is (thrown-with-msg? 28 | FileNotFoundException 29 | #"Configuration path './foo/bar/baz' must exist and must be readable." 30 | (bootstrap-services-with-cli-data [test-service] {:config "./foo/bar/baz"})))) 31 | 32 | (testing "Can read values from a single .ini file" 33 | (with-app-with-cli-data app [test-service] {:config "./dev-resources/config/file/config.ini"} 34 | (let [test-svc (get-service app :ConfigTestService)] 35 | (is (= (test-fn test-svc [:foo :setting1]) "foo1")) 36 | (is (= (test-fn test-svc [:foo :setting2]) "foo2")) 37 | (is (= (test-fn test-svc [:bar :setting1]) "bar1")) 38 | 39 | (testing "`get-config` function" 40 | (is (= (test-fn2 test-svc) {:foo {:setting2 "foo2" 41 | :setting1 "foo1"} 42 | :bar {:setting1 "bar1"} 43 | :debug false})))))) 44 | 45 | (testing "Can read values from a single .edn file" 46 | (with-app-with-cli-data app [test-service] {:config "./dev-resources/config/file/config.edn"} 47 | (let [test-svc (get-service app :ConfigTestService)] 48 | (testing "`get-config` function" 49 | (is (= {:debug false 50 | :foo {:bar "barbar" 51 | :baz "bazbaz" 52 | :bam 42 53 | :bap {:boozle "boozleboozle" 54 | :bip [1 2 {:hi "there"} 3]}}} 55 | (test-fn2 test-svc))))))) 56 | 57 | (testing "Can parse comma-separated configs" 58 | (with-app-with-cli-data app [test-service] 59 | {:config (str "./dev-resources/config/mixeddir/baz.ini," 60 | "./dev-resources/config/mixeddir/bar.conf")} 61 | (let [test-svc (get-service app :ConfigTestService)] 62 | (is (= {:debug false, :baz {:setting1 "baz1", :setting2 "baz2"} 63 | :bar {:junk "thingz" 64 | :nesty {:mappy {:hi "there" :stuff [1 2 {:how "areyou"} 3]}}}} 65 | (test-fn2 test-svc)))))) 66 | 67 | (testing "Conflicting comma-separated configs fail with error" 68 | (is (thrown-with-msg? 69 | IllegalArgumentException 70 | #"Duplicate configuration entry: \[:foo :baz\]" 71 | (bootstrap-services-with-cli-data [test-service] 72 | {:config (str "./dev-resources/config/conflictdir1/config.ini," 73 | "./dev-resources/config/conflictdir1/config.conf")})))) 74 | 75 | (testing "Error results when second of two comma-separated configs is malformed" 76 | (is (thrown-with-msg? 77 | FileNotFoundException 78 | #"Configuration path 'blob.conf' must exist and must be readable." 79 | (bootstrap-services-with-cli-data [test-service] 80 | {:config (str "./dev-resources/config/conflictdir1/config.ini," 81 | "blob.conf")})))) 82 | 83 | ;; NOTE: other individual file formats are tested in `typesafe-test` 84 | 85 | (testing "Can read values from a directory of .ini files" 86 | (with-app-with-cli-data app [test-service] {:config "./dev-resources/config/inidir"} 87 | (let [test-svc (get-service app :ConfigTestService)] 88 | (is (= (test-fn test-svc [:baz :setting1]) "baz1")) 89 | (is (= (test-fn test-svc [:baz :setting2]) "baz2")) 90 | (is (= (test-fn test-svc [:bam :setting1]) "bam1"))))) 91 | 92 | (testing "A proper default value is returned if a key can't be found" 93 | (with-app-with-cli-data app [test-service] {:config "./dev-resources/config/inidir"} 94 | (let [test-svc (get-service app :ConfigTestService)] 95 | (is (= (get-in-config test-svc [:doesnt :exist] "foo") "foo"))))) 96 | 97 | (testing "Can read values from a directory of mixed config files" 98 | (with-app-with-cli-data app [test-service] {:config "./dev-resources/config/mixeddir"} 99 | (let [test-svc (get-service app :ConfigTestService) 100 | cfg (test-fn2 test-svc)] 101 | (is (= {:debug false 102 | :taco {:burrito [1, 2] 103 | :nacho "cheese"} 104 | :foo {:bar "barbar" 105 | :baz "bazbaz" 106 | :meaningoflife 42} 107 | :baz {:setting1 "baz1" 108 | :setting2 "baz2"} 109 | :bar {:nesty {:mappy {:hi "there" 110 | :stuff [1 2 {:how "areyou"} 3]}} 111 | :junk "thingz"}} 112 | cfg))))) 113 | 114 | (testing "An error is thrown if duplicate settings exist" 115 | (doseq [invalid-config-dir ["./dev-resources/config/conflictdir1" 116 | "./dev-resources/config/conflictdir2" 117 | "./dev-resources/config/conflictdir3"]] 118 | (is (thrown-with-msg? 119 | IllegalArgumentException 120 | #"Duplicate configuration entry: \[:foo :baz\]" 121 | (bootstrap-services-with-cli-data [test-service] {:config invalid-config-dir}))))) 122 | 123 | (testing "Can call load-config directly" 124 | (is (= {:taco {:burrito [1, 2] 125 | :nacho "cheese"} 126 | :foo {:bar "barbar" 127 | :baz "bazbaz" 128 | :meaningoflife 42} 129 | :baz {:setting1 "baz1" 130 | :setting2 "baz2"} 131 | :bar {:nesty {:mappy {:hi "there" 132 | :stuff [1 2 {:how "areyou"} 3]}} 133 | :junk "thingz"}} 134 | (load-config "./dev-resources/config/mixeddir"))))) 135 | -------------------------------------------------------------------------------- /test/puppetlabs/trapperkeeper/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns puppetlabs.trapperkeeper.core-test 2 | (:require [clojure.test :refer :all] 3 | [puppetlabs.kitchensink.core :as ks] 4 | [puppetlabs.trapperkeeper.app :refer [get-service]] 5 | [puppetlabs.trapperkeeper.config :as config] 6 | [puppetlabs.trapperkeeper.internal :refer [parse-cli-args!]] 7 | [puppetlabs.trapperkeeper.services :refer [service]] 8 | [puppetlabs.trapperkeeper.testutils.bootstrap :as testutils] 9 | [puppetlabs.trapperkeeper.testutils.logging :as logging] 10 | [schema.test :as schema-test] 11 | [slingshot.slingshot :refer [try+]])) 12 | 13 | (use-fixtures :each schema-test/validate-schemas logging/reset-logging-config-after-test) 14 | 15 | (defprotocol FooService 16 | (foo [this])) 17 | 18 | (deftest dependency-error-handling 19 | (testing "missing service dependency throws meaningful message and logs error" 20 | (let [broken-service (service 21 | [[:MissingService f]] 22 | (init [this context] (f) context))] 23 | (logging/with-test-logging 24 | (is (thrown-with-msg? 25 | RuntimeException #"Service ':MissingService' not found" 26 | (testutils/bootstrap-services-with-empty-config [broken-service]))) 27 | (is (logged? #"Error during app buildup!" :error) 28 | "App buildup error message not logged")))) 29 | 30 | (testing "missing service function throws meaningful message and logs error" 31 | (let [test-service (service FooService 32 | [] 33 | (foo [this] "foo")) 34 | broken-service (service 35 | [[:FooService bar]] 36 | (init [this context] (bar) context))] 37 | (logging/with-test-logging 38 | (is (thrown-with-msg? 39 | RuntimeException 40 | #"Service function 'bar' not found in service 'FooService" 41 | (testutils/bootstrap-services-with-empty-config 42 | [test-service 43 | broken-service]))) 44 | (is (logged? #"Error during app buildup!" :error) 45 | "App buildup error message not logged"))) 46 | (try (macroexpand '(puppetlabs.trapperkeeper.services/service 47 | puppetlabs.trapperkeeper.core-test/FooService 48 | [] 49 | (init [this context] context))) 50 | (catch RuntimeException e 51 | (let [cause (-> e Throwable->map :cause)] 52 | (is (re-matches #"Service does not define function 'foo'.*" cause))))))) 53 | 54 | (deftest test-main 55 | (testing "Parsed CLI data" 56 | (let [bootstrap-file "/fake/path/bootstrap.cfg" 57 | config-dir "/fake/config/dir" 58 | restart-file "/fake/restart/file" 59 | cli-data (parse-cli-args! 60 | ["--debug" 61 | "--bootstrap-config" bootstrap-file 62 | "--config" config-dir 63 | "--restart-file" restart-file])] 64 | (is (= bootstrap-file (cli-data :bootstrap-config))) 65 | (is (= config-dir (cli-data :config))) 66 | (is (= restart-file (cli-data :restart-file))) 67 | (is (cli-data :debug)))) 68 | 69 | (testing "Invalid CLI data" 70 | (let [got-expected-exception (atom false)] 71 | (try+ 72 | (parse-cli-args! ["--invalid-argument"]) 73 | (catch map? m 74 | (is (contains? m :kind)) 75 | (is (= :cli-error (ks/without-ns (:kind m)))) 76 | (is (= :puppetlabs.kitchensink.core/cli-error (:kind m))) 77 | (is (contains? m :msg)) 78 | (is (re-find 79 | #"Unknown option.*--invalid-argument" 80 | (m :msg))) 81 | (reset! got-expected-exception true))) 82 | (is (true? @got-expected-exception)))) 83 | 84 | (testing "TK should allow the user to omit the --config arg" 85 | ;; Make sure args will be parsed if no --config arg is provided; will throw an exception if not 86 | (parse-cli-args! []) 87 | (is (true? true))) 88 | 89 | (testing "TK should use an empty config if none is specified" 90 | ;; Make sure data will be parsed if no path is provided; will throw an exception if not. 91 | (config/parse-config-data {}) 92 | (is (true? true)))) 93 | 94 | (deftest test-cli-args 95 | (testing "debug mode is off by default" 96 | (testutils/with-app-with-empty-config app [] 97 | (let [config-service (get-service app :ConfigService)] 98 | (is (false? (config/get-in-config config-service [:debug])))))) 99 | 100 | (testing "--debug puts TK in debug mode" 101 | (testutils/with-app-with-cli-args app [] ["--config" testutils/empty-config "--debug"] 102 | (let [config-service (get-service app :ConfigService)] 103 | (is (true? (config/get-in-config config-service [:debug])))))) 104 | 105 | (testing "TK should accept --plugins arg" 106 | ;; Make sure --plugins is allowed; will throw an exception if not. 107 | (parse-cli-args! ["--config" "yo mama" 108 | "--plugins" "some/plugin/directory"]))) 109 | 110 | (deftest restart-file-config 111 | (let [tk-config-file-with-restart (ks/temp-file "restart-global" ".conf") 112 | tk-restart-file "/my/tk-restart-file" 113 | cli-restart-file "/my/cli-restart-file"] 114 | (spit tk-config-file-with-restart 115 | (format "global: {\nrestart-file: %s\n}" tk-restart-file)) 116 | (testing "restart-file setting comes from TK config when CLI arg absent" 117 | (let [config (config/parse-config-data 118 | {:config (str tk-config-file-with-restart)})] 119 | (is (= tk-restart-file (get-in config [:global :restart-file]))))) 120 | (testing "restart-file setting comes from CLI arg when no TK config setting" 121 | (let [empty-tk-config-file (ks/temp-file "empty" ".conf") 122 | config (config/parse-config-data 123 | {:config (str empty-tk-config-file) 124 | :restart-file cli-restart-file})] 125 | (is (= cli-restart-file (get-in config [:global :restart-file]))))) 126 | (testing "restart-file setting comes from CLI arg even when set in TK config" 127 | (let [config (config/parse-config-data 128 | {:config (str tk-config-file-with-restart) 129 | :restart-file cli-restart-file})] 130 | (is (= cli-restart-file (get-in config [:global :restart-file]))))))) 131 | -------------------------------------------------------------------------------- /test/puppetlabs/trapperkeeper/custom_exit_behavior_test.clj: -------------------------------------------------------------------------------- 1 | (ns puppetlabs.trapperkeeper.custom-exit-behavior-test 2 | (:require 3 | [puppetlabs.trapperkeeper.core :as core])) 4 | 5 | (defprotocol CustomExitBehaviorTestService) 6 | 7 | (core/defservice custom-exit-behavior-test-service 8 | CustomExitBehaviorTestService 9 | [[:ShutdownService request-shutdown]] 10 | (init [this context] context) 11 | (start [this context] 12 | (request-shutdown {::core/exit {:messages [["Some excitement!\n" *out*] 13 | ["More excitement!\n" *err*]] 14 | :status 7}}) 15 | context) 16 | (stop [this context] context)) 17 | -------------------------------------------------------------------------------- /test/puppetlabs/trapperkeeper/examples/bootstrapping/test_services.clj: -------------------------------------------------------------------------------- 1 | (ns puppetlabs.trapperkeeper.examples.bootstrapping.test-services 2 | (:require [puppetlabs.trapperkeeper.core :refer [defservice]])) 3 | 4 | (defn invalid-service-graph-service 5 | [] 6 | {:test-service "hi"}) 7 | 8 | (defprotocol HelloWorldService 9 | (hello-world [this])) 10 | 11 | (defprotocol TestService 12 | (test-fn [this])) 13 | 14 | (defprotocol TestServiceTwo 15 | (test-fn-two [this])) 16 | 17 | (defprotocol TestServiceThree 18 | (test-fn-three [this])) 19 | 20 | (defservice hello-world-service 21 | HelloWorldService 22 | [] 23 | (hello-world [this] "hello world")) 24 | 25 | (defservice foo-test-service 26 | TestService 27 | [] 28 | (test-fn [this] :foo)) 29 | 30 | (defservice classpath-test-service 31 | TestService 32 | [] 33 | (test-fn [this] :classpath)) 34 | 35 | (defservice cwd-test-service 36 | TestService 37 | [] 38 | (test-fn [this] :cwd)) 39 | 40 | (defservice cli-test-service 41 | TestService 42 | [] 43 | (test-fn [this] :cli)) 44 | 45 | (defservice test-service-two 46 | TestServiceTwo 47 | [] 48 | (test-fn-two [this] :two)) 49 | (defservice test-service-two-duplicate 50 | TestServiceTwo 51 | [] 52 | (test-fn-two [this] :two)) 53 | 54 | (defservice test-service-three 55 | TestServiceThree 56 | [] 57 | (test-fn-three [this] :three)) 58 | -------------------------------------------------------------------------------- /test/puppetlabs/trapperkeeper/internal_test.clj: -------------------------------------------------------------------------------- 1 | (ns puppetlabs.trapperkeeper.internal-test 2 | (:require [clojure.test :refer :all] 3 | [puppetlabs.trapperkeeper.core :as tk] 4 | [puppetlabs.trapperkeeper.app :as tk-app] 5 | [puppetlabs.trapperkeeper.internal :as internal] 6 | [puppetlabs.trapperkeeper.testutils.bootstrap :as testutils] 7 | [puppetlabs.trapperkeeper.testutils.logging :as logging])) 8 | 9 | (deftest test-queued-restarts 10 | (testing "main lifecycle and calls to `restart-tk-apps` are not executed concurrently" 11 | (let [boot-promise (promise) 12 | lifecycle-events (atom []) 13 | svc (tk/service 14 | [] 15 | (init [this context] 16 | (swap! lifecycle-events conj :init) 17 | context) 18 | (start [this context] 19 | @boot-promise 20 | (swap! lifecycle-events conj :start) 21 | context) 22 | (stop [this context] 23 | (swap! lifecycle-events conj :stop) 24 | context)) 25 | config-fn (constantly {}) 26 | app (internal/build-app* [svc] config-fn) 27 | main-thread (future (internal/boot-services-for-app* app))] 28 | (while (< (count @lifecycle-events) 1) 29 | (Thread/yield)) 30 | (is (= [:init] @lifecycle-events)) 31 | (is (not (realized? main-thread))) 32 | (let [restart1-scheduled (promise) 33 | restart1-thread (future (internal/restart-tk-apps [app]) 34 | (deliver restart1-scheduled true)) 35 | restart2-scheduled (promise) 36 | restart2-thread (future (internal/restart-tk-apps [app]) 37 | (deliver restart2-scheduled true))] 38 | @restart1-scheduled 39 | (is (= [:init] @lifecycle-events)) 40 | @restart1-thread 41 | @restart2-scheduled 42 | (is (= [:init] @lifecycle-events)) 43 | @restart2-thread) 44 | 45 | (deliver boot-promise true) 46 | @main-thread 47 | (while (< (count @lifecycle-events) 8) 48 | (Thread/yield)) 49 | (is (= [:init :start :stop :init :start :stop :init :start] 50 | @lifecycle-events)) 51 | (tk-app/stop app) 52 | (is (= [:init :start :stop :init :start :stop :init :start :stop] 53 | @lifecycle-events))))) 54 | 55 | (deftest test-max-queued-restarts 56 | (let [stop-promise (promise) 57 | lifecycle-events (atom []) 58 | svc (tk/service 59 | [] 60 | (init [this context] 61 | (swap! lifecycle-events conj :init) 62 | context) 63 | (start [this context] 64 | (swap! lifecycle-events conj :start) 65 | context) 66 | (stop [this context] 67 | @stop-promise 68 | (swap! lifecycle-events conj :stop) 69 | context)) 70 | app (testutils/bootstrap-services-with-config 71 | [svc] 72 | {})] 73 | 74 | ;; the first restart will be picked up by the async worker, but it will 75 | ;; block on the 'stop-promise', so no more work can be picked up off of the 76 | ;; queue 77 | (internal/restart-tk-apps [app]) 78 | 79 | ;; now we issue how ever many restarts we need to to fill up the queue 80 | (dotimes [_i internal/max-pending-lifecycle-events] 81 | (internal/restart-tk-apps [app])) 82 | 83 | ;; now we choose some arbitrary number of additional restarts to request, 84 | ;; and confirm that we get a log message indicating that they were rejected 85 | (dotimes [_i 3] 86 | (logging/with-test-logging 87 | (internal/restart-tk-apps [app]) 88 | 89 | (is (logged? (format "Ignoring new SIGHUP restart requests; too many requests queued (%s)" 90 | internal/max-pending-lifecycle-events) 91 | :warn) 92 | "Missing expected log message when too many HUP requests queued"))) 93 | 94 | ;; now we unblock all of the queued restarts 95 | (deliver stop-promise true) 96 | 97 | ;; and validate that the life cycle events match up to that number of restarts 98 | (let [expected-lifecycle-events (->> [:stop :init :start] ; each restart will add these 99 | (repeat (+ 1 internal/max-pending-lifecycle-events)) 100 | (apply concat) 101 | (concat [:init :start]) ; here is the initial init/start 102 | vec)] 103 | (while (< (count @lifecycle-events) (count expected-lifecycle-events)) 104 | (Thread/yield)) 105 | (is (= expected-lifecycle-events @lifecycle-events)) 106 | 107 | ;; now we stop the app 108 | (tk-app/stop app) 109 | ;; and make sure that we got one last :stop 110 | (is (= (conj expected-lifecycle-events :stop) 111 | @lifecycle-events))))) -------------------------------------------------------------------------------- /test/puppetlabs/trapperkeeper/logging_test.clj: -------------------------------------------------------------------------------- 1 | (ns puppetlabs.trapperkeeper.logging-test 2 | (:require [clojure.java.io :as io] 3 | clojure.stacktrace 4 | [clojure.test :refer :all] 5 | [clojure.tools.logging :as log] 6 | [puppetlabs.trapperkeeper.logging :as tk-logging] 7 | [puppetlabs.trapperkeeper.testutils.logging :refer :all] 8 | [schema.test :as schema-test]) 9 | (:import (ch.qos.logback.classic Level))) 10 | 11 | (use-fixtures :each reset-logging-config-after-test schema-test/validate-schemas) 12 | 13 | (deftest test-catch-all-logger 14 | (testing "catch-all-logger ensures that message from an exception is logged" 15 | (with-test-logging 16 | ;; Prevent the stacktrace from being printed out 17 | (with-redefs [clojure.stacktrace/print-cause-trace (fn [_e] nil)] 18 | (tk-logging/catch-all-logger 19 | (Exception. "This exception is expected; testing error logging") 20 | "this is my error message")) 21 | (is (logged? #"this is my error message" :error))))) 22 | 23 | 24 | (deftest with-test-logging-on-separate-thread 25 | (testing "test-logging captures log messages from `future` threads" 26 | (with-test-logging 27 | (let [log-future (future 28 | (log/error "yo yo yo"))] 29 | @log-future 30 | (is (logged? #"yo yo yo" :error))))) 31 | (testing "threading doesn't break stuff" 32 | (with-test-logging 33 | (let [done? (promise)] 34 | (.start (Thread. (fn [] 35 | (log/info "test thread") 36 | (deliver done? true)))) 37 | (is (true? @done?)) 38 | (is (logged? #"test thread" :info)))))) 39 | 40 | (deftest with-test-logging-and-duplicate-log-lines 41 | (testing "test-logging captures matches duplicate lines when specified" 42 | (with-test-logging 43 | (log/error "duplicate message") 44 | (log/error "duplicate message") 45 | (log/warn "duplicate message") 46 | (log/warn "single message") 47 | (testing "single line only match" 48 | (is (not (logged? #"duplicate message"))) ;; original behavior of the fn, default behavior 49 | (is (logged? #"duplicate message" :warn false))) 50 | (testing "disabling single line match, enabling multiple line match" 51 | (is (logged? #"duplicate message" :error true)) 52 | (is (logged? #"duplicate message" nil true)) 53 | (testing "still handles single matches" 54 | (is (logged? #"single message" nil true)) 55 | (is (logged? #"single message" :warn true))))))) 56 | 57 | (deftest test-logging-configuration 58 | (testing "Calling `configure-logging!` with a logback.xml file" 59 | (tk-logging/configure-logging! "./dev-resources/logging/logback-debug.xml") 60 | (is (= (Level/DEBUG) (.getLevel (tk-logging/root-logger))))) 61 | 62 | (testing "Calling `configure-logging!` with another logback.xml file 63 | in case the default logging level is DEBUG" 64 | (tk-logging/configure-logging! "./dev-resources/logging/logback-warn.xml") 65 | (is (= (Level/WARN) (.getLevel (tk-logging/root-logger))))) 66 | 67 | (testing "a logging config file isn't required" 68 | ;; This looks strange, but we're trying to make sure that there are 69 | ;; no exceptions thrown when we configure logging without a log config file. 70 | (is (= nil (tk-logging/configure-logging! nil)))) 71 | 72 | (testing "support for logback evaluator filters" 73 | ;; This logging config file configures some fancy logback EvaluatorFilters, 74 | ;; and writes the log output to a file in `target/test`. 75 | (tk-logging/configure-logging! "./dev-resources/logging/logback-evaluator-filter.xml") 76 | (log/info "Hi! I should get filtered.") 77 | (log/info "Hi! I shouldn't get filtered.") 78 | (log/info (IllegalStateException. "OMGOMG") "Hi! I have an exception that should get filtered.") 79 | (with-open [reader (io/reader "./target/test/logback-evaluator-filter-test.log")] 80 | (let [lines (line-seq reader)] 81 | (is (= 1 (count lines))) 82 | (is (re-matches #".*Hi! I shouldn't get filtered\..*" (first lines))))))) 83 | 84 | (deftest test-logs-matching 85 | (let [log-lines '([puppetlabs.trapperkeeper.logging-test :info nil "log message1 at info"] 86 | [puppetlabs.trapperkeeper.logging-test :debug nil "log message1 at debug"] 87 | [puppetlabs.trapperkeeper.logging-test :warn nil "log message2 at warn"])] 88 | 89 | (testing "logs-matching can filter on message" 90 | ;; ignore deprecations 91 | #_:clj-kondo/ignore 92 | (is (= 2 (count (logs-matching #"log message1" log-lines))))) 93 | 94 | (testing "logs-matching can filter on message and level" 95 | ;; ignore deprecations 96 | #_:clj-kondo/ignore 97 | (is (= 1 (count (logs-matching #"log message1" log-lines :debug)))) 98 | #_:clj-kondo/ignore 99 | (is (= "log message1 at debug" (-> (logs-matching #"log message1" log-lines :debug) first :message))) 100 | #_:clj-kondo/ignore 101 | (is (empty? (logs-matching #"log message2" log-lines :info)))))) 102 | -------------------------------------------------------------------------------- /test/puppetlabs/trapperkeeper/plugins_test.clj: -------------------------------------------------------------------------------- 1 | (ns puppetlabs.trapperkeeper.plugins-test 2 | (:require [clojure.java.io :refer [file resource]] 3 | [clojure.test :refer :all] 4 | [puppetlabs.trapperkeeper.app :refer [service-graph]] 5 | [puppetlabs.trapperkeeper.plugins :as plugins] 6 | [puppetlabs.trapperkeeper.testutils.bootstrap :refer [bootstrap-with-empty-config]] 7 | [schema.test :as schema-test])) 8 | 9 | (use-fixtures :once schema-test/validate-schemas) 10 | 11 | (deftest test-jars-in-dir 12 | (let [jars (plugins/jars-in-dir (file "plugin-test-resources/plugins"))] 13 | (is (= 1 (count jars))) 14 | (is (= "plugin-test-resources/plugins/test-service.jar" (.getPath (first jars)))))) 15 | 16 | (deftest test-bad-directory 17 | (testing "TK throws an exception if --plugins is provided with a dir that does not exist." 18 | (is (thrown-with-msg? 19 | IllegalArgumentException 20 | #".*directory.*does not exist.*" 21 | (bootstrap-with-empty-config ["--plugins" "/this/does/not/exist"]))))) 22 | 23 | (deftest test-no-duplicates 24 | (testing "duplicate test passes on .jar with just a service in it" 25 | ;; `verify-no-duplicate-resources` throws an exception if a duplicate is found. 26 | (plugins/verify-no-duplicate-resources (file "plugin-test-resources/plugins/test-service.jar")))) 27 | 28 | (deftest test-duplicates 29 | (testing "duplicate test fails when an older version of kitchensink is included" 30 | (is (thrown-with-msg? 31 | IllegalArgumentException 32 | #".*Class or namespace.*found in both.*" 33 | (plugins/verify-no-duplicate-resources 34 | (file "plugin-test-resources/bad-plugins")))))) 35 | 36 | (deftest test-plugin-service 37 | (testing "TK can load and use service defined in plugin .jar" 38 | (let [app (bootstrap-with-empty-config 39 | ["--plugins" "./plugin-test-resources/plugins" 40 | "--bootstrap-config" "./dev-resources/bootstrapping/plugin/bootstrap.cfg"]) 41 | service-fn (-> (service-graph app) 42 | :PluginTestService 43 | :moo)] 44 | (is (= "This message comes from the plugin test service." (service-fn))) 45 | ;; Can it also load resources from that jar 46 | (is (resource "test_services/plugin_test_services.clj"))))) 47 | -------------------------------------------------------------------------------- /test/puppetlabs/trapperkeeper/services/config/typesafe_test.clj: -------------------------------------------------------------------------------- 1 | (ns puppetlabs.trapperkeeper.services.config.typesafe-test 2 | (:require [puppetlabs.config.typesafe :as ts] 3 | [clojure.test :refer :all] 4 | [schema.test :as schema-test])) 5 | 6 | (use-fixtures :once schema-test/validate-schemas) 7 | 8 | (deftest configfile->map-test 9 | (testing "can parse .properties file with nested data structures" 10 | (let [cfg (ts/config-file->map "./dev-resources/config/file/config.properties")] 11 | (is (= {:foo {:bar "barbar" 12 | :baz "bazbaz" 13 | :bam 42 14 | :bap {:boozle "boozleboozle"}}} 15 | cfg)))) 16 | (testing "can parse .json file with nested data structures" 17 | (let [cfg (ts/config-file->map "./dev-resources/config/file/config.json")] 18 | (is (= {:foo {:bar "barbar" 19 | :baz "bazbaz" 20 | :bam 42 21 | :bap {:boozle "boozleboozle" 22 | :bip [1 2 {:hi "there"} 3]}}} 23 | cfg)))) 24 | (testing "can parse .conf file with nested data structures" 25 | (let [cfg (ts/config-file->map "./dev-resources/config/file/config.conf")] 26 | (is (= {:foo {:bar "barbar" 27 | :baz "bazbaz" 28 | :bam 42 29 | :bap {:boozle "boozleboozle" 30 | :bip [1 2 {:hi "there"} 3]}}} 31 | cfg))))) -------------------------------------------------------------------------------- /test/puppetlabs/trapperkeeper/services/nrepl/nrepl_service_test.clj: -------------------------------------------------------------------------------- 1 | (ns puppetlabs.trapperkeeper.services.nrepl.nrepl-service-test 2 | (:require [clojure.test :refer :all] 3 | [nrepl.core :as repl] 4 | [puppetlabs.trapperkeeper.testutils.bootstrap :refer [with-app-with-config]] 5 | [puppetlabs.trapperkeeper.services.nrepl.nrepl-service :as nrepl-service] 6 | [schema.test :as schema-test])) 7 | 8 | (use-fixtures :once schema-test/validate-schemas) 9 | 10 | (deftest test-nrepl-config 11 | (letfn [(process-config-fn [enabled] 12 | (->> {:nrepl {:enabled enabled}} 13 | (partial get-in) 14 | nrepl-service/process-config 15 | :enabled?))] 16 | (testing "Should support string value for `enabled?`" 17 | (is (= true (process-config-fn "true"))) 18 | (is (= false (process-config-fn "false")))) 19 | (testing "Should support boolean value for `enabled?`" 20 | (is (= true (process-config-fn true))) 21 | (is (= false (process-config-fn false)))))) 22 | 23 | (deftest test-nrepl-service 24 | (testing "An nREPL service has been started" 25 | (with-app-with-config app 26 | [nrepl-service/nrepl-service] 27 | {:nrepl {:port 7888 28 | :host "0.0.0.0" 29 | :enabled "true"}} 30 | (is (= [2] (with-open [conn (repl/connect :port 7888)] 31 | (-> (repl/client conn 1000) 32 | (repl/message {:op "eval" :code "(+ 1 1)"}) 33 | (repl/response-values)))))))) 34 | 35 | (deftest test-nrepl-service-2 36 | (testing "An nREPL service without middlewares has been started" 37 | (with-app-with-config app 38 | [nrepl-service/nrepl-service] 39 | {:nrepl {:port 7888 40 | :host "0.0.0.0" 41 | :enabled "true" 42 | :middlewares []}} 43 | (is (= [2] (with-open [conn (repl/connect :port 7888)] 44 | (-> (repl/client conn 1000) 45 | (repl/message {:op "eval" :code "(+ 1 1)"}) 46 | (repl/response-values)))))))) 47 | 48 | (deftest test-nrepl-service-3 49 | (testing "An nREPL service with test middleware has been started" 50 | (with-app-with-config app 51 | [nrepl-service/nrepl-service] 52 | {:nrepl {:port 7888 53 | :host "0.0.0.0" 54 | :enabled "true" 55 | :middlewares "[puppetlabs.trapperkeeper.services.nrepl.nrepl-test-send-middleware/send-test]"}} 56 | (is (= "success" (with-open [conn (repl/connect :port 7888)] 57 | (:test (first (-> (repl/client conn 1000) 58 | (repl/message {:op "middlewaretest"})))))))) 59 | (with-app-with-config app 60 | [nrepl-service/nrepl-service] 61 | {:nrepl {:port 7888 62 | :host "0.0.0.0" 63 | :enabled "true" 64 | :middlewares ["puppetlabs.trapperkeeper.services.nrepl.nrepl-test-send-middleware/send-test"]}} 65 | (is (= "success" (with-open [conn (repl/connect :port 7888)] 66 | (:test (first (-> (repl/client conn 1000) 67 | (repl/message {:op "middlewaretest"})))))))))) 68 | -------------------------------------------------------------------------------- /test/puppetlabs/trapperkeeper/services/nrepl/nrepl_test_send_middleware.clj: -------------------------------------------------------------------------------- 1 | (ns puppetlabs.trapperkeeper.services.nrepl.nrepl-test-send-middleware 2 | (:require [nrepl.transport :as t] 3 | [nrepl.middleware :refer [set-descriptor!]] 4 | [nrepl.misc :refer [response-for]])) 5 | 6 | (defn send-test 7 | [h] 8 | (fn [{:keys [op transport] :as msg}] 9 | (if (= "middlewaretest" op) 10 | (t/send transport (response-for msg :status "done" :test "success")) 11 | (h msg)))) 12 | 13 | (set-descriptor! 14 | #'send-test 15 | {:requires #{} 16 | :expects #{}}) 17 | -------------------------------------------------------------------------------- /test/puppetlabs/trapperkeeper/services_internal_test.clj: -------------------------------------------------------------------------------- 1 | (ns puppetlabs.trapperkeeper.services-internal-test 2 | (:import (clojure.lang IFn)) 3 | (:require [clojure.test :refer :all] 4 | [plumbing.fnk.pfnk :as pfnk] 5 | [schema.core :as schema] 6 | [schema.test :as schema-test] 7 | [puppetlabs.trapperkeeper.app :as app] 8 | [puppetlabs.trapperkeeper.services :refer [service service-map] :as svcs] 9 | [puppetlabs.trapperkeeper.services-internal :as si] 10 | [puppetlabs.trapperkeeper.testutils.bootstrap :refer 11 | [with-app-with-empty-config]])) 12 | 13 | (use-fixtures :once schema-test/validate-schemas) 14 | 15 | (deftest service-forms-test 16 | (testing "should support forms that include protocol" 17 | (is (= {:dependencies [] 18 | :fns '() 19 | :service-protocol-sym 'Foo} 20 | (si/find-prot-and-deps-forms! '(Foo []))))) 21 | (testing "should support forms that do not include protocol" 22 | (is (= {:dependencies [] 23 | :fns '() 24 | :service-protocol-sym nil} 25 | (si/find-prot-and-deps-forms! '([]))))) 26 | (testing "result should include vector of fn forms if provided" 27 | (is (= {:dependencies [] 28 | :fns '((fn1 [] "fn1") (fn2 [] "fn2")) 29 | :service-protocol-sym 'Foo} 30 | (si/find-prot-and-deps-forms! 31 | '(Foo [] (fn1 [] "fn1") (fn2 [] "fn2"))))) 32 | (is (= {:dependencies [] 33 | :fns '((fn1 [] "fn1") (fn2 [] "fn2")) 34 | :service-protocol-sym nil} 35 | (si/find-prot-and-deps-forms! 36 | '([] (fn1 [] "fn1") (fn2 [] "fn2")))))) 37 | (testing "should throw exception if the first form is not the protocol symbol or dependency vector" 38 | (is (thrown-with-msg? 39 | IllegalArgumentException 40 | #"Invalid service definition; first form must be protocol or dependency list; found '\"hi\"'" 41 | (si/find-prot-and-deps-forms! '("hi" []))))) 42 | (testing "should throw exception if the first form is a protocol sym and the second is not a dependency vector" 43 | (is (thrown-with-msg? 44 | IllegalArgumentException 45 | #"Invalid service definition; expected dependency list following protocol, found: '\"hi\"'" 46 | (si/find-prot-and-deps-forms! '(Foo "hi"))))) 47 | (testing "should throw an exception if all remaining forms are not seqs" 48 | (is (thrown-with-msg? 49 | IllegalArgumentException 50 | #"Invalid service definition; expected function definitions following dependency list, invalid value: '\"hi\"'" 51 | (si/find-prot-and-deps-forms! '(Foo [] (fn1 [] "fn1") "hi")))))) 52 | 53 | (defn local-resolve 54 | "Resolve symbol in current (services-internal-test) namespace" 55 | [sym] 56 | {:pre [(symbol? sym)]} 57 | (ns-resolve 58 | 'puppetlabs.trapperkeeper.services-internal-test 59 | sym)) 60 | 61 | (defprotocol EmptyProtocol) 62 | (def NonProtocolSym "hi") 63 | 64 | (deftest protocol-syms-test 65 | (testing "should not throw exception if protocol exists" 66 | (is (si/protocol? 67 | (si/validate-protocol-sym! 68 | 'EmptyProtocol 69 | (local-resolve 'EmptyProtocol))))) 70 | 71 | (testing "should throw exception if service protocol sym is not resolvable" 72 | (is (thrown-with-msg? 73 | IllegalArgumentException 74 | #"Unrecognized service protocol 'UndefinedSym'" 75 | (si/validate-protocol-sym! 'UndefinedSym (local-resolve 'UndefinedSym))))) 76 | 77 | (testing "should throw exception if service protocol symbol is resolveable but does not resolve to a protocol" 78 | (is (thrown-with-msg? 79 | IllegalArgumentException 80 | #"Specified service protocol 'NonProtocolSym' does not appear to be a protocol!" 81 | (si/validate-protocol-sym! 'NonProtocolSym (local-resolve 'NonProtocolSym)))))) 82 | 83 | (deftest build-fns-map-test 84 | (testing "minimal services may not define functions other than lifecycle functions" 85 | (is (thrown-with-msg? 86 | IllegalArgumentException 87 | #"Service attempts to define function 'foo', but does not provide protocol" 88 | (si/build-fns-map! nil [] ['init 'start] 89 | '((init [this context] context) 90 | (start [this context] context) 91 | (foo [this] "foo"))))))) 92 | 93 | (defprotocol Service1 94 | (service1-fn [this])) 95 | 96 | (defprotocol Service2 97 | (service2-fn [this])) 98 | 99 | (defprotocol BadServiceProtocol 100 | (start [this])) 101 | 102 | (deftest invalid-fns-test 103 | (testing "should throw an exception if there is no definition of a function in the protocol" 104 | (is (thrown-with-msg? 105 | IllegalArgumentException 106 | #"Service does not define function 'service1-fn', which is required by protocol 'Service1'" 107 | (si/parse-service-forms! 108 | ['init 'start] 109 | (cons 'puppetlabs.trapperkeeper.services-internal-test/Service1 110 | '([] (init [this context] context))))))) 111 | (testing "should throw an exception if there is a definition for a function that is not in the protocol" 112 | (is (thrown-with-msg? 113 | IllegalArgumentException 114 | #"Service attempts to define function 'foo', which does not exist in protocol 'Service1'" 115 | (si/parse-service-forms! 116 | ['init 'start] 117 | (cons 'puppetlabs.trapperkeeper.services-internal-test/Service1 118 | '([] (foo [this] "foo"))))))) 119 | (testing "should throw an exception if the protocol includes a function with the same name as a lifecycle function" 120 | (is (thrown-with-msg? 121 | IllegalArgumentException 122 | #"Service protocol 'BadServiceProtocol' includes function named 'start', which conflicts with lifecycle function by same name" 123 | (si/parse-service-forms! 124 | ['init 'start] 125 | (cons 'puppetlabs.trapperkeeper.services-internal-test/BadServiceProtocol 126 | '([] (start [this] "foo")))))))) 127 | 128 | (deftest prismatic-functionality-test 129 | (testing "prismatic fnk is initialized properly" 130 | (let [service1 (service Service1 131 | [] 132 | (init [this context] context) 133 | (start [this context] context) 134 | (service1-fn [this] "Foo!")) 135 | service2 (service Service2 136 | [[:Service1 service1-fn]] 137 | (init [this context] context) 138 | (start [this context] context) 139 | (service2-fn [this] "Bar!")) 140 | s1-graph (service-map service1) 141 | s2-graph (service-map service2)] 142 | (is (map? s1-graph)) 143 | (let [graph-keys (keys s1-graph)] 144 | (is (= (count graph-keys) 1)) 145 | (is (= (first graph-keys) :Service1))) 146 | 147 | (let [service-fnk (:Service1 s1-graph) 148 | depends (pfnk/input-schema service-fnk) 149 | provides (pfnk/output-schema service-fnk)] 150 | (is (ifn? service-fnk)) 151 | (is (= depends {schema/Keyword schema/Any 152 | :tk-app-context schema/Any 153 | :tk-service-refs schema/Any})) 154 | (is (= provides {:service1-fn IFn}))) 155 | 156 | (is (map? s2-graph)) 157 | (let [graph-keys (keys s2-graph)] 158 | (is (= (count graph-keys) 1)) 159 | (is (= (first graph-keys) :Service2))) 160 | 161 | (let [service-fnk (:Service2 s2-graph) 162 | depends (pfnk/input-schema service-fnk) 163 | provides (pfnk/output-schema service-fnk) 164 | fnk-instance (service-fnk {:Service1 {:service1-fn identity} 165 | :tk-app-context (atom {}) 166 | :tk-service-refs (atom {})}) 167 | s2-fn (:service2-fn fnk-instance)] 168 | (is (ifn? service-fnk)) 169 | (is (= depends {schema/Keyword schema/Any 170 | :tk-app-context schema/Any 171 | :tk-service-refs schema/Any 172 | :Service1 {schema/Keyword schema/Any 173 | :service1-fn schema/Any}})) 174 | (is (= provides {:service2-fn IFn})) 175 | (is (= "Bar!" (s2-fn))))))) 176 | 177 | (defprotocol EmptyService) 178 | 179 | (deftest explicit-service-symbol-test 180 | (testing "can explicitly pass `service` a service symbol via internal API" 181 | (let [empty-service (service {:service-symbol foo/bar} EmptyService [])] 182 | (with-app-with-empty-config app [empty-service] 183 | (let [svc (app/get-service app :EmptyService)] 184 | (is (= :EmptyService (svcs/service-id svc))) 185 | (is (= (symbol "foo" "bar") (svcs/service-symbol svc)))))))) -------------------------------------------------------------------------------- /test/puppetlabs/trapperkeeper/services_namespaces_test/ns1.clj: -------------------------------------------------------------------------------- 1 | (ns puppetlabs.trapperkeeper.services-namespaces-test.ns1) 2 | 3 | (defprotocol FooService 4 | (foo [this])) -------------------------------------------------------------------------------- /test/puppetlabs/trapperkeeper/services_namespaces_test/ns2.clj: -------------------------------------------------------------------------------- 1 | (ns puppetlabs.trapperkeeper.services-namespaces-test.ns2 2 | (:require 3 | [clojure.test :refer :all] 4 | [puppetlabs.kitchensink.testutils.fixtures :refer [with-no-jvm-shutdown-hooks]] 5 | [puppetlabs.trapperkeeper.core :as trapperkeeper] 6 | [puppetlabs.trapperkeeper.services-namespaces-test.ns1 :as ns1] 7 | [puppetlabs.trapperkeeper.testutils.bootstrap :refer [bootstrap-services-with-empty-config]] 8 | [schema.test :as schema-test])) 9 | 10 | (use-fixtures :once schema-test/validate-schemas with-no-jvm-shutdown-hooks) 11 | 12 | (trapperkeeper/defservice foo-service 13 | ns1/FooService 14 | [] 15 | (foo [this] "foo")) 16 | 17 | (deftest test-service-namespaces 18 | (testing "can boot service defined in different namespace than protocol" 19 | (bootstrap-services-with-empty-config [foo-service]) 20 | (is (true? true)))) 21 | -------------------------------------------------------------------------------- /test/puppetlabs/trapperkeeper/signal_handling_test.clj: -------------------------------------------------------------------------------- 1 | (ns puppetlabs.trapperkeeper.signal-handling-test 2 | (:require 3 | [puppetlabs.trapperkeeper.core :as core])) 4 | 5 | (defn- start-test [context get-in-config] 6 | (let [continue? (atom true) 7 | thread (future 8 | (try ;; future just discards top-level exceptions 9 | (while @continue? 10 | (let [target (get-in-config [:signal-test-target])] 11 | (assert target) 12 | (Thread/sleep 200) 13 | (spit target "exciting"))) 14 | (catch Throwable ex 15 | (prn ex) 16 | (throw ex))))] 17 | (assoc context 18 | :finish-signal-test 19 | (fn exit-signal-test [] 20 | (reset! continue? false) 21 | @thread)))) 22 | 23 | (defn- stop-test [{:keys [finish-signal-test] :as context}] 24 | (finish-signal-test) 25 | context) 26 | 27 | (defprotocol SignalHandlingTestService) 28 | 29 | (core/defservice signal-handling-test-service 30 | SignalHandlingTestService 31 | [[:ConfigService get-in-config]] 32 | (init [this context] context) 33 | (start [this context] (start-test context get-in-config)) 34 | (stop [this context] (stop-test context))) 35 | -------------------------------------------------------------------------------- /test/puppetlabs/trapperkeeper/testutils/bootstrap.clj: -------------------------------------------------------------------------------- 1 | (ns puppetlabs.trapperkeeper.testutils.bootstrap 2 | (:require [me.raynes.fs :as fs] 3 | [puppetlabs.trapperkeeper.core :as tk] 4 | [puppetlabs.trapperkeeper.app :as tk-app] 5 | [puppetlabs.kitchensink.testutils :as ks-testutils] 6 | [puppetlabs.trapperkeeper.bootstrap :as bootstrap] 7 | [puppetlabs.trapperkeeper.config :as config] 8 | [puppetlabs.trapperkeeper.internal :as internal])) 9 | 10 | (def empty-config "./target/empty.ini") 11 | (fs/touch empty-config) 12 | 13 | (defn bootstrap-services-with-config 14 | [services config] 15 | (internal/throw-app-error-if-exists! 16 | (tk/boot-services-with-config services config))) 17 | 18 | (defmacro with-app-with-config 19 | [app services config & body] 20 | `(ks-testutils/with-no-jvm-shutdown-hooks 21 | (let [~app (bootstrap-services-with-config ~services ~config)] 22 | (try 23 | ~@body 24 | (finally 25 | (tk-app/stop ~app true)))))) 26 | 27 | (defn bootstrap-services-with-cli-data 28 | [services cli-data] 29 | (internal/throw-app-error-if-exists! 30 | (tk/boot-services-with-config-fn services 31 | #(config/parse-config-data cli-data)))) 32 | 33 | (defmacro with-app-with-cli-data 34 | [app services cli-data & body] 35 | `(ks-testutils/with-no-jvm-shutdown-hooks 36 | (let [~app (bootstrap-services-with-cli-data ~services ~cli-data)] 37 | (try 38 | ~@body 39 | (finally 40 | (tk-app/stop ~app true)))))) 41 | 42 | (defn bootstrap-services-with-cli-args 43 | [services cli-args] 44 | (bootstrap-services-with-cli-data services 45 | (internal/parse-cli-args! cli-args))) 46 | 47 | (defmacro with-app-with-cli-args 48 | [app services cli-args & body] 49 | `(ks-testutils/with-no-jvm-shutdown-hooks 50 | (let [~app (bootstrap-services-with-cli-args ~services ~cli-args)] 51 | (try 52 | ~@body 53 | (finally 54 | (tk-app/stop ~app true)))))) 55 | 56 | (defn bootstrap-services-with-empty-config 57 | [services] 58 | (bootstrap-services-with-cli-data services {:config empty-config})) 59 | 60 | (defmacro with-app-with-empty-config 61 | [app services & body] 62 | `(ks-testutils/with-no-jvm-shutdown-hooks 63 | (let [~app (bootstrap-services-with-empty-config ~services)] 64 | (try 65 | ~@body 66 | (finally 67 | (tk-app/stop ~app true)))))) 68 | 69 | (defn bootstrap-with-empty-config 70 | ([] 71 | (bootstrap-with-empty-config [])) 72 | ([other-args] 73 | (-> other-args 74 | (conj "--config" empty-config) 75 | (internal/parse-cli-args!) 76 | (tk/boot-with-cli-data) 77 | (internal/throw-app-error-if-exists!)))) 78 | 79 | (defn parse-and-bootstrap 80 | ([bootstrap-config] 81 | (parse-and-bootstrap bootstrap-config {:config empty-config})) 82 | ([bootstrap-config cli-data] 83 | (-> bootstrap-config 84 | (bootstrap/parse-bootstrap-config!) 85 | (bootstrap-services-with-cli-data cli-data)))) 86 | -------------------------------------------------------------------------------- /test/puppetlabs/trapperkeeper/testutils/logging_test.clj: -------------------------------------------------------------------------------- 1 | (ns puppetlabs.trapperkeeper.testutils.logging-test 2 | (:require 3 | [clojure.test :refer :all] 4 | [clojure.tools.logging :as log] 5 | [puppetlabs.kitchensink.core :as kitchensink] 6 | [puppetlabs.trapperkeeper.logging :refer [reset-logging root-logger-name]] 7 | [puppetlabs.trapperkeeper.testutils.logging :as tgt :refer [event->map]]) 8 | (:import 9 | (org.slf4j LoggerFactory))) 10 | 11 | ;; Without this, "lein test NAMESPACE" and :only invocations may fail. 12 | (use-fixtures :once (fn [f] (reset-logging) (f))) 13 | 14 | (deftest with-log-level-and-logging-to-atom 15 | (let [expected {:logger "puppetlabs.trapperkeeper.testutils.logging-test" 16 | :level :info 17 | :message "wlta-test" 18 | :exception nil}] 19 | (let [log (atom [])] 20 | (tgt/with-log-level root-logger-name :error 21 | (tgt/with-logging-to-atom root-logger-name log 22 | (log/info "wlta-test")) 23 | (is (not-any? #(= expected %) (map event->map @log))))) 24 | (let [log (atom [])] 25 | (tgt/with-log-level root-logger-name :info 26 | (tgt/with-logging-to-atom root-logger-name log 27 | (log/info "wlta-test")) 28 | (is (some #(= expected %) (map event->map @log))))))) 29 | 30 | (def call-with-started #'puppetlabs.trapperkeeper.testutils.logging/call-with-started) 31 | (def find-logger #'puppetlabs.trapperkeeper.testutils.logging/find-logger) 32 | (def log-event-listener #'puppetlabs.trapperkeeper.testutils.logging/log-event-listener) 33 | 34 | (defn get-appenders [logger] 35 | (iterator-seq (.iteratorForAppenders logger))) 36 | 37 | (deftest with-additional-log-appenders 38 | (let [log (atom []) 39 | logger (find-logger root-logger-name) 40 | uuid (kitchensink/uuid) 41 | original-appenders (get-appenders logger) 42 | new-appender (doto (log-event-listener 43 | (fn [event] (swap! log conj event))) 44 | .start) 45 | expected {:logger "puppetlabs.trapperkeeper.testutils.logging-test" 46 | :level :error 47 | :message uuid 48 | :exception nil}] 49 | (call-with-started 50 | [new-appender] 51 | #(tgt/with-additional-log-appenders root-logger-name [new-appender] 52 | (is (= (set (cons new-appender original-appenders)) 53 | (set (get-appenders logger)))) 54 | (log/error uuid))) 55 | (is (= (set original-appenders) 56 | (set (get-appenders logger)))) 57 | (is (some #(= expected %) (map event->map @log))))) 58 | 59 | (deftest with-log-appenders 60 | (let [log (atom []) 61 | logger (find-logger root-logger-name) 62 | uuid (kitchensink/uuid) 63 | original-appenders (get-appenders logger) 64 | new-appender (doto (log-event-listener 65 | (fn [event] (swap! log conj event))) 66 | .start) 67 | expected {:logger "puppetlabs.trapperkeeper.testutils.logging-test" 68 | :level :error 69 | :message uuid 70 | :exception nil}] 71 | (call-with-started 72 | [new-appender] 73 | ;; ignore deprecation 74 | #_:clj-kondo/ignore 75 | #(tgt/with-log-appenders root-logger-name 76 | [new-appender] 77 | (is (= [new-appender] (get-appenders logger))) 78 | (log/error uuid))) 79 | (is (= (set original-appenders) 80 | (set (get-appenders logger)))) 81 | (is (some #(= expected %) (map event->map @log))))) 82 | 83 | (deftest with-log-event-listeners 84 | (let [log (atom []) 85 | uuid (kitchensink/uuid) 86 | expected {:logger "puppetlabs.trapperkeeper.testutils.logging-test" 87 | :level :info 88 | :message uuid 89 | :exception nil}] 90 | (tgt/with-log-level root-logger-name :info 91 | (tgt/with-log-event-listeners root-logger-name 92 | [(fn [event] (swap! log conj event))] 93 | (log/info uuid)) 94 | (is (some #(= expected %) (map event->map @log)))))) 95 | 96 | (deftest suppressing-log-unless-error 97 | (let [uuid (kitchensink/uuid) 98 | target (format "some random message %s" uuid)] 99 | (testing "log not dumped if uninteresting" 100 | (is (not (re-find (re-pattern target) 101 | (with-out-str 102 | (binding [*err* *out*] 103 | (tgt/with-log-suppressed-unless-notable 104 | (constantly false) 105 | (log/info target)))))))) 106 | (testing "log dumped if notable" 107 | (is (re-find (re-pattern target) 108 | (with-out-str 109 | (binding [*err* *out*] 110 | (tgt/with-log-suppressed-unless-notable 111 | #(= "lp0 on fire" (:message (event->map %))) 112 | (log/info target) 113 | (log/info "lp0 on fire"))))))))) 114 | 115 | (deftest with-test-logging 116 | (testing "basic matching" 117 | (doseq [[item test] [["foo" "foo" 118 | "barbar" #"rb" 119 | "baz" (fn [e] 120 | (and (= :trace (:level e)) 121 | (= "baz" (:message e))))]]] 122 | (tgt/with-test-logging 123 | (log/trace item) 124 | (is (logged? test))) 125 | (tgt/with-test-logging 126 | (log/trace "hapax legomenon") 127 | (is (not (tgt/logged? test)))))) 128 | (testing "level matches" 129 | (doseq [level @#'puppetlabs.trapperkeeper.testutils.logging/levels] 130 | (tgt/with-test-logging 131 | (log/log level "foo") 132 | (is (logged? "foo" level)))) 133 | ;; Does not match when logged above or below correct level 134 | (tgt/with-test-logging 135 | (log/debug "debug") 136 | (is (not (tgt/logged? #"debug" :warn)))) 137 | (tgt/with-test-logging 138 | (log/debug "debug") 139 | (is (not (tgt/logged? #"debug" :trace))))) 140 | (testing "captures parameterized slf4j messages" 141 | (tgt/with-test-logging 142 | (let [test-logger (LoggerFactory/getLogger "tk-test")] 143 | (.info test-logger "Log message: {}" "odelay") 144 | (is (tgt/logged? #"odelay")))))) 145 | 146 | (deftest with-test-logging-debug 147 | (testing "basic matching" 148 | (doseq [[item test] [["foo" "foo" 149 | "barbar" #"rb" 150 | "baz" (fn [e] 151 | (and (= :trace (:level e)) 152 | (= "baz" (:message e))))]]] 153 | (tgt/with-test-logging-debug 154 | (log/trace item) 155 | (is (logged? test))) 156 | (tgt/with-test-logging-debug 157 | (log/trace "hapax legomenon") 158 | (is (not (tgt/logged? test)))))) 159 | (testing "level matches" 160 | (doseq [level @#'puppetlabs.trapperkeeper.testutils.logging/levels] 161 | (tgt/with-test-logging-debug 162 | (log/log level "foo") 163 | (is (logged? "foo" level)))) 164 | (tgt/with-test-logging-debug 165 | (log/debug "debug") 166 | (is (not (tgt/logged? #"debug" :warn)))) 167 | (tgt/with-test-logging 168 | (log/debug "debug") 169 | (is (not (tgt/logged? #"debug" :trace))))) 170 | (testing "that events are logged to *err*" 171 | (tgt/with-test-logging-debug 172 | (let [err (with-out-str (binding [*err* *out*] 173 | (log/trace "foo")))] 174 | (is (re-matches #"\*\* Log entry: (.|\n)*" err)) 175 | (is (re-find #":logger " err)) 176 | (is (re-find #":level :trace" err)) 177 | (is (re-find #":exception nil" err)) 178 | (is (re-find #":message \"foo\"" err))) 179 | (is (logged? "foo"))))) 180 | 181 | (deftest with-logger-event-maps 182 | (let [expected {:logger "puppetlabs.trapperkeeper.testutils.logging-test" 183 | :level :error 184 | :message "wlgrem-test" 185 | :exception nil}] 186 | (tgt/with-logger-event-maps root-logger-name events 187 | (log/error "wlgrem-test") 188 | (is (some #(= expected %) @events))))) 189 | 190 | (deftest with-logged-event-maps 191 | (let [expected {:logger "puppetlabs.trapperkeeper.testutils.logging-test" 192 | :level :error 193 | :message "wlgdem-test" 194 | :exception nil}] 195 | (tgt/with-logged-event-maps events 196 | (log/error "wlgdem-test") 197 | (is (some #(= expected %) @events))))) 198 | -------------------------------------------------------------------------------- /tk: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -ueo pipefail 4 | 5 | usage() { echo "Usage: tk JVM_ARG ... -- TK_ARG ..."; } 6 | misuse() { usage 1>&2; exit 2; } 7 | 8 | jar_glob='trapperkeeper-*-SNAPSHOT-standalone.jar' 9 | 10 | # Believe last -cp wins for java, and here, any final -cp path will be 11 | # placed in front of the jar. 12 | 13 | cp='' 14 | jvm_args=() 15 | while test $# -gt 0; do 16 | case "$1" in 17 | -h|--help) 18 | usage 19 | exit 0 20 | ;; 21 | -cp) 22 | shift 23 | test $# -gt 0 || misuse 24 | cp="$1" 25 | shift 26 | ;; 27 | --) 28 | shift 29 | break 30 | ;; 31 | *) 32 | shift 33 | jvm_args+=("$1") 34 | ;; 35 | esac 36 | done 37 | 38 | if test "${TRAPPERKEEPER_JAR:-}"; then 39 | jar="$TRAPPERKEEPER_JAR" 40 | else 41 | # Find the standalone jar and make sure there's only one. 42 | # FIXME: minor race here between find runs 43 | # Use a bash array expansion to count the files so we don't have 44 | # to worry about strange paths (though admittedly unlikely here). 45 | shopt -s nullglob 46 | jars=(target/$jar_glob) 47 | shopt -u nullglob 48 | if test "${#jars[@]}" -gt 1; then 49 | echo "error: found more than one SNAPSHOT jar:" 1>&2 50 | find target -maxdepth 1 -name "$jar_glob" 1>&2 51 | exit 2 52 | fi 53 | jar="${jars[0]}" 54 | fi 55 | 56 | if ! test -e "$jar"; then 57 | printf 'Unable to find target/%s; have you run "lein uberjar"?\n' \ 58 | "$jar" 1>&2 59 | exit 2 60 | fi 61 | 62 | set -x 63 | if test "$cp"; then 64 | cp="$cp:$jar" 65 | else 66 | cp="$jar" 67 | fi 68 | 69 | exec java -cp "$cp" clojure.main -m puppetlabs.trapperkeeper.main "$@" 70 | --------------------------------------------------------------------------------