daemon-kit 0.1.7.9 → 0.1.7.10
Sign up to get free protection for your applications and to get access to all the features.
- data/Configuration.txt +43 -0
- data/History.txt +8 -0
- data/Manifest.txt +14 -0
- data/README.rdoc +26 -7
- data/Rakefile +9 -2
- data/RuoteParticipants.txt +113 -0
- data/app_generators/daemon_kit/daemon_kit_generator.rb +5 -5
- data/daemon_generators/ruote/USAGE +5 -0
- data/daemon_generators/ruote/ruote_generator.rb +67 -0
- data/daemon_generators/ruote/templates/config/amqp.yml +30 -0
- data/daemon_generators/ruote/templates/config/initializers/ruote.rb +13 -0
- data/daemon_generators/ruote/templates/config/ruote.yml +23 -0
- data/daemon_generators/ruote/templates/lib/daemon.rb +4 -0
- data/daemon_generators/ruote/templates/lib/sample.rb +26 -0
- data/daemon_generators/ruote/templates/libexec/daemon.rb +33 -0
- data/lib/daemon_kit.rb +9 -5
- data/lib/daemon_kit/application.rb +20 -0
- data/lib/daemon_kit/arguments.rb +4 -0
- data/lib/daemon_kit/config.rb +44 -6
- data/lib/daemon_kit/exceptions.rb +8 -0
- data/lib/daemon_kit/initializer.rb +47 -1
- data/lib/daemon_kit/ruote_participants.rb +119 -0
- data/lib/daemon_kit/ruote_pseudo_participant.rb +68 -0
- data/lib/daemon_kit/ruote_workitem.rb +169 -0
- data/spec/argument_spec.rb +19 -0
- data/spec/config_spec.rb +8 -6
- data/spec/initializer_spec.rb +3 -9
- data/test/test_ruote_generator.rb +45 -0
- metadata +40 -12
data/Configuration.txt
CHANGED
@@ -57,3 +57,46 @@ instance call also be modified from the command line using the special
|
|
57
57
|
|
58
58
|
This happens after <em>config/environment.rb</em> is processed, so all
|
59
59
|
command line arguments will overwrite those values.
|
60
|
+
|
61
|
+
=== Daemon umask
|
62
|
+
|
63
|
+
By default daemon processes run with a umask of 022, but this can be changed
|
64
|
+
on the command line or in +config/environment.rb+.
|
65
|
+
|
66
|
+
To set a more restrictive umask via command line arguments, you can start your
|
67
|
+
daemon like this:
|
68
|
+
|
69
|
+
$ ./bin/daemon start --config umask=0077
|
70
|
+
|
71
|
+
Or the same in +config/environment.rb+
|
72
|
+
|
73
|
+
DaemonKit::Initializer.run do |config|
|
74
|
+
# ...
|
75
|
+
|
76
|
+
# restrictive umask
|
77
|
+
config.umask = 0077
|
78
|
+
|
79
|
+
# ...
|
80
|
+
end
|
81
|
+
|
82
|
+
=== Privilege Separation
|
83
|
+
|
84
|
+
By default daemon processes run as the user that starts them, inheriting all
|
85
|
+
their privileges (or lack thereof). Getting daemon-kit to drop privileges
|
86
|
+
can currently only be done using command-line parameters, and only works
|
87
|
+
reliable on *nix (OSX seemed cranky at the time of testing).
|
88
|
+
|
89
|
+
$ ./bin/daemon start --config user=nobody --config group=nobody
|
90
|
+
|
91
|
+
Privileges are dropped at the earliest possible phase of starting the daemon.
|
92
|
+
|
93
|
+
Things to note on privilege separation:
|
94
|
+
|
95
|
+
* You generally have to be root to be able to perform this
|
96
|
+
* File system permissions for +log/+ needs to be correct
|
97
|
+
* Daemon-kit will only shed privileges on the +start+ command, not on +run+
|
98
|
+
* Make sure your code is secure if accepting stuff from the outside world
|
99
|
+
|
100
|
+
The implementation stems from the advice given by Joe Damato on his blog post
|
101
|
+
http://timetobleed.com/tag/privilege-escalation/
|
102
|
+
|
data/History.txt
CHANGED
@@ -1,3 +1,11 @@
|
|
1
|
+
== 0.1.7.10 (WIP)
|
2
|
+
|
3
|
+
* Ruote remote participants
|
4
|
+
* Allow process umask to be configured, defaults to 022
|
5
|
+
* Updates to DaemonKit::Config hashes
|
6
|
+
* Fixed argument parsing bug (reported by Mathijs Kwik (bluescreen303)
|
7
|
+
* Support for privilege separation (See Configuration.txt)
|
8
|
+
|
1
9
|
== 0.1.7.9 2009-06-22
|
2
10
|
|
3
11
|
* Backtraces only logged on unclean shutdown
|
data/Manifest.txt
CHANGED
@@ -6,6 +6,7 @@ Manifest.txt
|
|
6
6
|
PostInstall.txt
|
7
7
|
README.rdoc
|
8
8
|
Rakefile
|
9
|
+
RuoteParticipants.txt
|
9
10
|
TODO.txt
|
10
11
|
app_generators/daemon_kit/USAGE
|
11
12
|
app_generators/daemon_kit/daemon_kit_generator.rb
|
@@ -65,6 +66,14 @@ daemon_generators/rspec/templates/spec.rb
|
|
65
66
|
daemon_generators/rspec/templates/spec/spec.opts
|
66
67
|
daemon_generators/rspec/templates/spec/spec_helper.rb
|
67
68
|
daemon_generators/rspec/templates/tasks/rspec.rake
|
69
|
+
daemon_generators/ruote/USAGE
|
70
|
+
daemon_generators/ruote/ruote_generator.rb
|
71
|
+
daemon_generators/ruote/templates/config/amqp.yml
|
72
|
+
daemon_generators/ruote/templates/config/initializers/ruote.rb
|
73
|
+
daemon_generators/ruote/templates/config/ruote.yml
|
74
|
+
daemon_generators/ruote/templates/lib/daemon.rb
|
75
|
+
daemon_generators/ruote/templates/lib/sample.rb
|
76
|
+
daemon_generators/ruote/templates/libexec/daemon.rb
|
68
77
|
lib/daemon_kit.rb
|
69
78
|
lib/daemon_kit/abstract_logger.rb
|
70
79
|
lib/daemon_kit/amqp.rb
|
@@ -83,11 +92,15 @@ lib/daemon_kit/em.rb
|
|
83
92
|
lib/daemon_kit/error_handlers/base.rb
|
84
93
|
lib/daemon_kit/error_handlers/hoptoad.rb
|
85
94
|
lib/daemon_kit/error_handlers/mail.rb
|
95
|
+
lib/daemon_kit/exceptions.rb
|
86
96
|
lib/daemon_kit/initializer.rb
|
87
97
|
lib/daemon_kit/jabber.rb
|
88
98
|
lib/daemon_kit/nanite.rb
|
89
99
|
lib/daemon_kit/nanite/agent.rb
|
90
100
|
lib/daemon_kit/pid_file.rb
|
101
|
+
lib/daemon_kit/ruote_participants.rb
|
102
|
+
lib/daemon_kit/ruote_pseudo_participant.rb
|
103
|
+
lib/daemon_kit/ruote_workitem.rb
|
91
104
|
lib/daemon_kit/safety.rb
|
92
105
|
lib/daemon_kit/tasks.rb
|
93
106
|
lib/daemon_kit/tasks/environment.rake
|
@@ -122,6 +135,7 @@ test/test_generator_helper.rb
|
|
122
135
|
test/test_helper.rb
|
123
136
|
test/test_jabber_generator.rb
|
124
137
|
test/test_nanite_agent_generator.rb
|
138
|
+
test/test_ruote_generator.rb
|
125
139
|
vendor/tmail-1.2.3/tmail.rb
|
126
140
|
vendor/tmail-1.2.3/tmail/address.rb
|
127
141
|
vendor/tmail-1.2.3/tmail/attachments.rb
|
data/README.rdoc
CHANGED
@@ -13,9 +13,11 @@ Using simple built-in generators it is easy to created evented and non-evented d
|
|
13
13
|
|
14
14
|
Supported generators:
|
15
15
|
|
16
|
-
*
|
17
|
-
*
|
18
|
-
*
|
16
|
+
* XMPP bot (non-evented)
|
17
|
+
* AMQP consumer (evented)
|
18
|
+
* Nanite agent
|
19
|
+
* Cron-style daemon
|
20
|
+
* ruote remote participants
|
19
21
|
|
20
22
|
== Features/Problems
|
21
23
|
|
@@ -24,17 +26,21 @@ Supported generators:
|
|
24
26
|
|
25
27
|
== Synopsis
|
26
28
|
|
27
|
-
$
|
29
|
+
$ daemon_kit -h
|
30
|
+
|
31
|
+
Get some help
|
32
|
+
|
33
|
+
$ daemon_kit [/path/to/your/daemon] [options]
|
28
34
|
|
29
35
|
The above command generates a skeleton daemon environment for you to adapt.
|
30
36
|
|
31
|
-
$
|
37
|
+
$ daemon_kit [/path/to/your/daemon] -i jabber
|
32
38
|
|
33
39
|
Use the 'jabber' generator instead of the default one.
|
34
40
|
|
35
41
|
== Generators
|
36
42
|
|
37
|
-
Currently
|
43
|
+
Currently six generators exist: default, jabber, amqp, cron, nanite & ruote
|
38
44
|
|
39
45
|
The default generator creates a simple daemon with an infinite loop inside that you can adapt.
|
40
46
|
|
@@ -50,6 +56,14 @@ The cron generator creates a simple daemon that leverages the "rufus-scheduler":
|
|
50
56
|
|
51
57
|
The AMQP generator creates a simple daemon that has all the stub code and configuration in place to help you write AMQP consumers quickly and effectively. The generated daemon relies on the presence of the "amqp":http://github.com/tmm1/amqp gem.
|
52
58
|
|
59
|
+
=== Nanite Agent Generator
|
60
|
+
|
61
|
+
The "nanite":http://github.com/ezmobius/nanite agent generator gets you up and running with nanite agents very quickly.
|
62
|
+
|
63
|
+
=== ruote Remote Participants
|
64
|
+
|
65
|
+
The "ruote":http://openwfe.rubyforge.org remote participant generator speeds up the development of workflow participants that run outside of the Ruby process that houses the engine. Daemon-kit handles all the communication and delegation logic, allowing you to focus purely on your participant's activities.
|
66
|
+
|
53
67
|
== Requirements
|
54
68
|
|
55
69
|
* Ruby 1.8.6
|
@@ -61,8 +75,10 @@ The AMQP generator creates a simple daemon that has all the stub code and config
|
|
61
75
|
Depending on the generator you choose for your daemon, it might require additional gems to run.
|
62
76
|
|
63
77
|
* jabber - xmpp4r-simple[http://xmpp4r-simple.rubyforge.org]
|
64
|
-
* cron - rufus-scheduler[http://github.com/jmettraux/rufus-scheduler]
|
78
|
+
* cron - rufus-scheduler[http://github.com/jmettraux/rufus-scheduler] (at least version 2.0.0)
|
65
79
|
* amqp - amqp[http://github.com/tmm1/amqp]
|
80
|
+
* nanite - nanite[http://github.com/ezmobius/nanite]
|
81
|
+
* ruote - none, although ruote[http://openwfe.rubyforge.org] should probably be running somewhere
|
66
82
|
|
67
83
|
== Install
|
68
84
|
|
@@ -84,6 +100,9 @@ Stable versions, when released are available directly from Rubyforge:
|
|
84
100
|
|
85
101
|
* Configuration.txt
|
86
102
|
* Deployment.txt
|
103
|
+
* Logging.txt
|
104
|
+
* RuoteParticipants.txt
|
105
|
+
* http://www.opensourcery.co.za/tag/daemon-kit/
|
87
106
|
|
88
107
|
== License
|
89
108
|
|
data/Rakefile
CHANGED
@@ -1,9 +1,16 @@
|
|
1
|
-
|
1
|
+
require 'rubygems'
|
2
|
+
gem 'hoe', '>= 2.1.0'
|
3
|
+
require 'hoe'
|
4
|
+
require 'fileutils'
|
2
5
|
require File.dirname(__FILE__) + '/lib/daemon_kit'
|
3
6
|
|
7
|
+
Hoe.plugin :newgem
|
8
|
+
Hoe.plugin :website
|
9
|
+
# Hoe.plugin :cucumberfeatures
|
10
|
+
|
4
11
|
# Generate all the Rake tasks
|
5
12
|
# Run 'rake -T' to see list of generated tasks (from gem root directory)
|
6
|
-
$hoe = Hoe.
|
13
|
+
$hoe = Hoe.spec('daemon-kit') do |p|
|
7
14
|
p.summary = 'Daemon Kit aims to simplify creating Ruby daemons by providing a sound application skeleton (through a generator), task specific generators (jabber bot, etc) and robust environment management code.'
|
8
15
|
p.developer('Kenneth Kalmer', 'kenneth.kalmer@gmail.com')
|
9
16
|
p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
|
@@ -0,0 +1,113 @@
|
|
1
|
+
= Writing remote ruote participants with daemon-kit
|
2
|
+
|
3
|
+
daemon-kit is an ideal housing for remote ruote participants, providing a lot
|
4
|
+
of convenience in terms of receiving and sending workitems, delegating work to
|
5
|
+
pseudo-participant classes, handling configuration of the communication channel
|
6
|
+
between ruote and the remote participant, and much more.
|
7
|
+
|
8
|
+
== What is ruote?
|
9
|
+
|
10
|
+
Ruote is a Ruby workflow engine. It is a powerful tool for defining, running and
|
11
|
+
orchestrating business processes.
|
12
|
+
|
13
|
+
* http://openwferu.rubyforge.org/
|
14
|
+
* http://www.opensourcery.co.za/2009/03/04/ruote-in-20-minutes/
|
15
|
+
* http://www.opensourcery.co.za/2009/07/06/driving-business-processes-in-ruby/
|
16
|
+
|
17
|
+
== What are remote participants?
|
18
|
+
|
19
|
+
Remote participants are participants that perform their work in a different
|
20
|
+
Ruby processes from the one running the engine. This is useful in two cases,
|
21
|
+
possibily many more, that involves autonomous participants.
|
22
|
+
|
23
|
+
* Autonomous participants located on remote servers, driven by identity
|
24
|
+
* Clustering autonomous participants to process workitems from a queue
|
25
|
+
|
26
|
+
To learn more about the differences between local and remote participants
|
27
|
+
please see http://openwferu.rubyforge.org/part.html
|
28
|
+
|
29
|
+
Currently on the AMQP components are in place in daemon-kit, with XMPP coming
|
30
|
+
soon.
|
31
|
+
|
32
|
+
== Creating a remote participant with daemon-kit
|
33
|
+
|
34
|
+
Generate your daemon using the 'ruote' generator:
|
35
|
+
|
36
|
+
$ daemon_kit partd -i ruote
|
37
|
+
|
38
|
+
Make sure you have the JSON gem install, and the AMQP gem as well.
|
39
|
+
|
40
|
+
== Configuring the daemon
|
41
|
+
|
42
|
+
You need to review +config/ruote.yml+ to specify the AMQP queues that the daemon
|
43
|
+
will subscribe to for receiving workitems. You'll also need to configure the
|
44
|
+
AMQP gem be updating +config/amqp.yml+
|
45
|
+
|
46
|
+
The generated daemon in +libexec/+ already defaults to using AMQP as a transport
|
47
|
+
for workitems.
|
48
|
+
|
49
|
+
== Writing pseudo-participants
|
50
|
+
|
51
|
+
Pseudo-participants in daemon-kit are pure Ruby classes. Implement your classes
|
52
|
+
in +lib/+ and require them from +lib/<daemon_name>.rb+.
|
53
|
+
|
54
|
+
Register your classes as pseudo-participants by registering them in the daemon
|
55
|
+
file in +libexec+, just as the Sample class is registered in the generated
|
56
|
+
code. Your class will be instantiated upon registration, and will be re-used
|
57
|
+
for every incoming workitem passed to it.
|
58
|
+
|
59
|
+
All your public methods in the pseudo-participant classes should be accept
|
60
|
+
a single parameter, which is a ruote workitem in pure Hash form.
|
61
|
+
|
62
|
+
== Wiring up the remote participant in ruote
|
63
|
+
|
64
|
+
See the complete code here: http://gist.github.com/144861
|
65
|
+
|
66
|
+
A sample process definition might look something like this:
|
67
|
+
|
68
|
+
class QuoteProcess < OpenWFE::ProcessDefinition
|
69
|
+
sequence do
|
70
|
+
kit :command => '/sample/quote', :queue => 'work1'
|
71
|
+
|
72
|
+
console
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
+kit+ in the above process definition is registered with the ruote engine as an
|
77
|
+
AMQPParticipant. The AMQPParticipant delivers workitems to the specified AMQP
|
78
|
+
queue.
|
79
|
+
|
80
|
+
Based on the values in +config/ruote.yml+, your daemon will be subscribed to
|
81
|
+
those queues.
|
82
|
+
|
83
|
+
The second part is delegating the workitem inside the daemon to the correct
|
84
|
+
pseudo-participant. This is handled by the +:command+ parameter in the process
|
85
|
+
definition. DaemonKit::Workitem looks at the command parameter of the incoming
|
86
|
+
workitem, and then finds a registered pseudo-participant instance and calls the
|
87
|
+
requested method on that class.
|
88
|
+
|
89
|
+
The +:command+ parameter follows the following convention (stolen shamelessly
|
90
|
+
from Nanite):
|
91
|
+
|
92
|
+
:command => '/class_name/method_name'
|
93
|
+
|
94
|
+
When classes are registered, the name of the class is downcased and camel-case
|
95
|
+
words are separated by underscores. Method names are not changed, but methods
|
96
|
+
are required to be public.
|
97
|
+
|
98
|
+
== Processing workitems and replying to the engine
|
99
|
+
|
100
|
+
The methods called in the pseudo-participants receive a single parameter, a
|
101
|
+
ruote workitem as a hash. The participant is then free to analyze the hash
|
102
|
+
and perform the appropriate actions required. The return value of the method
|
103
|
+
is discarded, and the workitem is returned back to the engine. If the method
|
104
|
+
modified the workitem, these changes will be sent along as well.
|
105
|
+
|
106
|
+
== Random other notes
|
107
|
+
|
108
|
+
Apart from configuring the AMPQ client (or XMPP in future) and the ruote.yml
|
109
|
+
file, daemon developers don't need to worry about anything related to receiving
|
110
|
+
workitems or sending replies.
|
111
|
+
|
112
|
+
Our aim is to allow you to swap between participants on both sides of the
|
113
|
+
transport without changing any of your code.
|
@@ -3,7 +3,7 @@ class DaemonKitGenerator < RubiGen::Base
|
|
3
3
|
DEFAULT_SHEBANG = File.join(Config::CONFIG['bindir'],
|
4
4
|
Config::CONFIG['ruby_install_name'])
|
5
5
|
|
6
|
-
VALID_GENERATORS = ['default', 'jabber', 'cron', 'amqp', 'nanite_agent']
|
6
|
+
VALID_GENERATORS = ['default', 'jabber', 'cron', 'amqp', 'nanite_agent', 'ruote']
|
7
7
|
|
8
8
|
DEPLOYERS = ['none', 'capistrano']
|
9
9
|
|
@@ -76,13 +76,13 @@ class DaemonKitGenerator < RubiGen::Base
|
|
76
76
|
m.directory "config/post-daemonize"
|
77
77
|
m.file "config/post-daemonize/readme", "config/post-daemonize/readme"
|
78
78
|
m.directory "script"
|
79
|
-
m.file "script/destroy", "script/destroy"
|
80
|
-
m.file "script/console", "script/console"
|
81
|
-
m.file "script/generate", "script/generate"
|
79
|
+
m.file "script/destroy", "script/destroy", script_options
|
80
|
+
m.file "script/console", "script/console", script_options
|
81
|
+
m.file "script/generate", "script/generate", script_options
|
82
82
|
|
83
83
|
# Libraries
|
84
84
|
m.directory "lib"
|
85
|
-
m.file "lib/daemon.rb", "lib/#{daemon_name}.rb"
|
85
|
+
m.file "lib/daemon.rb", "lib/#{daemon_name}.rb", :collision => :skip
|
86
86
|
|
87
87
|
# Tasks
|
88
88
|
m.directory "tasks"
|
@@ -0,0 +1,67 @@
|
|
1
|
+
class RuoteGenerator < RubiGen::Base
|
2
|
+
|
3
|
+
default_options :author => nil
|
4
|
+
|
5
|
+
attr_reader :name
|
6
|
+
|
7
|
+
def initialize(runtime_args, runtime_options = {})
|
8
|
+
super
|
9
|
+
usage if args.empty?
|
10
|
+
@name = args.shift
|
11
|
+
extract_options
|
12
|
+
end
|
13
|
+
|
14
|
+
def manifest
|
15
|
+
record do |m|
|
16
|
+
# Ensure appropriate folder(s) exists
|
17
|
+
m.directory ''
|
18
|
+
|
19
|
+
# Create stubs
|
20
|
+
# m.template "template.rb.erb", "some_file_after_erb.rb"
|
21
|
+
# m.template_copy_each ["template.rb", "template2.rb"]
|
22
|
+
# m.template_copy_each ["template.rb", "template2.rb"], "some/path"
|
23
|
+
# m.file "file", "some_file_copied"
|
24
|
+
# m.file_copy_each ["path/to/file", "path/to/file2"]
|
25
|
+
# m.file_copy_each ["path/to/file", "path/to/file2"], "some/path"
|
26
|
+
|
27
|
+
m.directory 'config'
|
28
|
+
m.template 'config/amqp.yml', 'config/amqp.yml'
|
29
|
+
m.template 'config/ruote.yml', 'config/ruote.yml'
|
30
|
+
m.directory 'config/initializers'
|
31
|
+
m.template 'config/initializers/ruote.rb', "config/initializers/#{name}.rb"
|
32
|
+
|
33
|
+
m.directory 'lib'
|
34
|
+
m.template 'lib/daemon.rb', "lib/#{name}.rb"
|
35
|
+
m.template 'lib/sample.rb', 'lib/sample.rb'
|
36
|
+
m.directory 'libexec'
|
37
|
+
m.template 'libexec/daemon.rb', "libexec/#{name}-daemon.rb"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
protected
|
42
|
+
def banner
|
43
|
+
<<-EOS
|
44
|
+
Creates a ...
|
45
|
+
|
46
|
+
USAGE: #{$0} #{spec.name} name
|
47
|
+
EOS
|
48
|
+
end
|
49
|
+
|
50
|
+
def add_options!(opts)
|
51
|
+
# opts.separator ''
|
52
|
+
# opts.separator 'Options:'
|
53
|
+
# For each option below, place the default
|
54
|
+
# at the top of the file next to "default_options"
|
55
|
+
# opts.on("-a", "--author=\"Your Name\"", String,
|
56
|
+
# "Some comment about this option",
|
57
|
+
# "Default: none") { |o| options[:author] = o }
|
58
|
+
# opts.on("-v", "--version", "Show the #{File.basename($0)} version number and quit.")
|
59
|
+
end
|
60
|
+
|
61
|
+
def extract_options
|
62
|
+
# for each option, extract it into a local variable (and create an "attr_reader :author" at the top)
|
63
|
+
# Templates can access these value via the attr_reader-generated methods, but not the
|
64
|
+
# raw instance variable value.
|
65
|
+
# @author = options[:author]
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# AMQP client configuration file for ruote remote participants. If you are not
|
2
|
+
# planning to use the AMQP participant/listener pair in ruote, you can safely
|
3
|
+
# delete this file.
|
4
|
+
|
5
|
+
# These values will be used to configure the ampq gem, any values
|
6
|
+
# omitted will let the gem use it's own defaults.
|
7
|
+
|
8
|
+
# The configuration specifies the following keys:
|
9
|
+
# * user - Username for the broker
|
10
|
+
# * pass - Password for the broker
|
11
|
+
# * host - Hostname where the broker is running
|
12
|
+
# * vhost - Vhost to connect to
|
13
|
+
# * port - Port where the broker is running
|
14
|
+
# * ssl - Use ssl or not
|
15
|
+
# * timeout - Timeout
|
16
|
+
|
17
|
+
defaults: &defaults
|
18
|
+
user: guest
|
19
|
+
pass: guest
|
20
|
+
host: localhost
|
21
|
+
vhost: /
|
22
|
+
|
23
|
+
development:
|
24
|
+
<<: *defaults
|
25
|
+
|
26
|
+
test:
|
27
|
+
<<: *defaults
|
28
|
+
|
29
|
+
production:
|
30
|
+
<<: *defaults
|