' \
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 | [](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 |
--------------------------------------------------------------------------------