jackal-stacks 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4b71b344dfac97c00282df6e96bde5f3d413a74e
4
+ data.tar.gz: 9b787df9e775186d5333de90f3eacfd9076a1135
5
+ SHA512:
6
+ metadata.gz: 55393fe0bb6bfd1b26e6841eac37a4674b384560a90688bfa8094c7fc3ec01a13841cbd0ed8e265136ba90c811bd4c4c4a1cbba1ebcc4a141e88c875ede9609d
7
+ data.tar.gz: dadc256d8c3b29ec838e608141f4a088577ff593790c3aad5d9b22e3526136d893dc990b78f5c2e9ab897f842cd44b9ab4edfd9e41c5dbbb495f0d1101fd1df8
data/CHANGELOG.md ADDED
@@ -0,0 +1,2 @@
1
+ # v0.1.0
2
+ * Initial release
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,25 @@
1
+ # Contributing
2
+
3
+ ## Branches
4
+
5
+ ### `master` branch
6
+
7
+ The master branch is the current stable released version.
8
+
9
+ ### `develop` branch
10
+
11
+ The develop branch is the current edge of development.
12
+
13
+ ## Pull requests
14
+
15
+ * https://github.com/carnivore-rb/jackal-stacks/pulls
16
+
17
+ Please base all pull requests of the `develop` branch. Merges to
18
+ `master` only occur through the `develop` branch. Pull requests
19
+ based on `master` will likely be cherry picked.
20
+
21
+ ## Issues
22
+
23
+ Need to report an issue? Use the github issues:
24
+
25
+ * https://github.com/carnivore-rb/jackal-stacks/issues
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2015 Chris Roberts
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,15 @@
1
+ # Jackal Stacks
2
+
3
+ Build stacks \o/
4
+
5
+ ## Requirements
6
+
7
+ This service assumes the payload information provided via the code
8
+ fetcher service.
9
+
10
+ ## Configuration
11
+
12
+ # Info
13
+
14
+ * Repository: https://github.com/carnivore-rb/jackal-stacks
15
+ * IRC: Freenode @ #carnivore
@@ -0,0 +1,17 @@
1
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__)) + '/lib/'
2
+ require 'jackal-stacks/version'
3
+ Gem::Specification.new do |s|
4
+ s.name = 'jackal-stacks'
5
+ s.version = Jackal::Stacks::VERSION.version
6
+ s.summary = 'Message processing helper'
7
+ s.author = 'Chris Roberts'
8
+ s.email = 'code@chrisroberts.org'
9
+ s.homepage = 'https://github.com/carnivore-rb/jackal-stacks'
10
+ s.description = 'Build stuff!'
11
+ s.require_path = 'lib'
12
+ s.license = 'Apache 2.0'
13
+ s.add_dependency 'jackal'
14
+ s.add_dependency 'librarian-chef'
15
+ s.add_dependency 'sfn'
16
+ s.files = Dir['lib/**/*'] + %w(jackal-stacks.gemspec README.md CHANGELOG.md CONTRIBUTING.md LICENSE)
17
+ end
@@ -0,0 +1,13 @@
1
+ require 'sfn'
2
+ require 'jackal'
3
+
4
+ module Jackal
5
+ module Stacks
6
+ autoload :Builder, 'jackal-stacks/builder'
7
+ autoload :StackCommon, 'jackal-stacks/stack_common'
8
+ autoload :Wrecker, 'jackal-stacks/wrecker'
9
+ end
10
+ end
11
+
12
+ require 'jackal-stacks/formatter'
13
+ require 'jackal-stacks/version'
@@ -0,0 +1,242 @@
1
+ require 'jackal-stacks'
2
+
3
+ module Jackal
4
+ module Stacks
5
+ # Stack builder
6
+ class Builder < Callback
7
+
8
+ include StackCommon
9
+
10
+ # Setup callback
11
+ def setup(*_)
12
+ require 'sfn'
13
+ require 'bogo-ui'
14
+ require 'stringio'
15
+ require 'openssl'
16
+ # Ensure we can build the API at startup
17
+ stacks_api
18
+ end
19
+
20
+ # Determine validity of message
21
+ #
22
+ # @param message [Carnivore::Message]
23
+ # @return [Truthy, Falsey]
24
+ def valid?(message)
25
+ super do |payload|
26
+ payload.get(:data, :stacks, :builder) &&
27
+ payload.get(:data, :stacks, :template) &&
28
+ payload.get(:data, :stacks, :asset) &&
29
+ allowed?(payload)
30
+ end
31
+ end
32
+
33
+ # @return [String] working directory
34
+ def working_directory
35
+ memoize(:working_directory, :direct) do
36
+ FileUtils.mkdir_p(dir = config.fetch(:working_directory, '/tmp/jackal-stack-builder'))
37
+ dir
38
+ end
39
+ end
40
+
41
+ # @return [String] fresh working directory
42
+ def workspace(payload)
43
+ File.join(working_directory, payload[:id])
44
+ end
45
+
46
+ # Build or update stacks
47
+ #
48
+ # @param message [Carnivore::Message]
49
+ def execute(message)
50
+ failure_wrap(message) do |payload|
51
+ directory = asset_store.unpack(asset_store.get(payload.get(:data, :stacks, :asset)), workspace(payload))
52
+ begin
53
+ unless(payload.get(:data, :stacks, :name))
54
+ payload.set(:data, :stacks, :name, stack_name(payload))
55
+ end
56
+ store_stable_asset(payload, directory)
57
+ begin
58
+ stack = stacks_api.stacks.get(payload.get(:data, :stacks, :name))
59
+ rescue
60
+ stack = nil
61
+ end
62
+ if(stack)
63
+ info "Stack currently exists. Applying update [#{stack}]"
64
+ run_stack(payload, directory, :update)
65
+ payload.set(:data, :stacks, :updated, true)
66
+ else
67
+ info "Stack does not currently exist. Building new stack [#{payload.get(:data, :stacks, :name)}]"
68
+ init_provider(payload)
69
+ run_stack(payload, directory, :create)
70
+ payload.set(:data, :stacks, :created, true)
71
+ end
72
+ ensure
73
+ FileUtils.rm_rf(directory)
74
+ end
75
+ job_completed(:stacks, payload, message)
76
+ end
77
+ end
78
+
79
+ # Build configuration arguments for Sfn::Command execution
80
+ #
81
+ # @param payload [Smash]
82
+ # @param directory [String] directory to unpacked asset
83
+ # @return [Smash] stack command options hash
84
+ def build_stack_args(payload, directory)
85
+ Smash.new(
86
+ :base_directory => File.join(directory, 'cloudformation'),
87
+ :parameters => load_stack_parameters(payload, directory),
88
+ :ui => Bogo::Ui.new(
89
+ :app_name => 'JackalStacks',
90
+ :defaults => true,
91
+ :output_to => StringIO.new('')
92
+ ),
93
+ :interactive_parameters => false,
94
+ :nesting_bucket => config.get(:orchestration, :bucket_name),
95
+ :apply_nesting => true,
96
+ :processing => true,
97
+ :options => {
98
+ :disable_rollback => true,
99
+ :capabilities => ['CAPABILITY_IAM']
100
+ },
101
+ :credentials => config.get(:orchestration, :api, :credentials),
102
+ :file => payload.get(:data, :stacks, :template),
103
+ :file_path_prompt => false,
104
+ :poll => false
105
+ )
106
+ end
107
+
108
+ # Extract any custom parameters from asset store if available,
109
+ # and merge any parameters provided via payload, and finally
110
+ # merge any parameters provided via configuration
111
+ #
112
+ # @param payload [Smash]
113
+ # @param directory [String]
114
+ # @note parameter precedence:
115
+ # * Hook URL parameters
116
+ # * Payload parameters
117
+ # * Stacks file parameters
118
+ # * Service configuration parameters
119
+ def load_stack_parameters(payload, directory)
120
+ params = Smash.new
121
+ stacks_file = load_stacks_file(payload, directory)
122
+ s_namespace = determine_namespace(payload)
123
+ template = payload.get(:data, :stacks, :template)
124
+ params.deep_merge!(payload.fetch(:data, :webhook, :query, :stacks, :parameters, Smash.new))
125
+ params.deep_merge!(payload.fetch(:data, :stacks, :parameters, Smash.new))
126
+ params.deep_merge!(
127
+ stacks_file.fetch(s_namespace, template, :parameters,
128
+ stacks_file.fetch(:default, template, :parameters, Smash.new)
129
+ )
130
+ )
131
+ params.deep_merge!(
132
+ config.fetch(:parameter_overrides, s_namespace, template,
133
+ config.fetch(:parameter_overrides, :default, template, Smash.new)
134
+ )
135
+ )
136
+ params
137
+ end
138
+
139
+ # Parse the `.stacks` file if available
140
+ #
141
+ # @param payload [Smash]
142
+ # @param directory [String] path to unpacked asset directory
143
+ # @return [Smash]
144
+ def load_stacks_file(payload, directory)
145
+ stacks_path = File.join(directory, '.stacks')
146
+ if(File.exists?(stacks_path))
147
+ Bogo::Config.new(file_path).data
148
+ else
149
+ Smash.new
150
+ end
151
+ end
152
+
153
+ # Perform stack action
154
+ #
155
+ # @param payload [Smash]
156
+ # @param directory [String] directory to unpacked asset
157
+ # @param action [Symbol, String] :create or :update
158
+ # @return [TrueClass]
159
+ def run_stack(payload, directory, action)
160
+ unless([:create, :update].include?(action.to_sym))
161
+ abort ArgumentError.new("Invalid action argument `#{action}`. Expecting `create` or `update`!")
162
+ end
163
+ args = build_stack_args(payload, directory)
164
+ stack_name = payload.get(:data, :stacks, :name)
165
+ Sfn::Command.const_get(action.to_s.capitalize).new(args, [stack_name]).execute!
166
+ true
167
+ end
168
+
169
+ # Check if this payload is allowed to be processed based on
170
+ # defined restrictions within the configuration
171
+ #
172
+ # @param payload [Smash]
173
+ # @return [TrueClass, FalseClass]
174
+ def allowed?(payload)
175
+ !!determine_namespace(payload)
176
+ end
177
+
178
+ # Initialize provider if instructed via config
179
+ #
180
+ # @param payload [Smash]
181
+ # @note this currently init's chef related items
182
+ def init_provider(payload)
183
+ if(config.get(:init, :chef, :validator) || config.get(:init, :chef, :encrypted_secret))
184
+ bucket = stacks_api.api_for(:storage).buckets.get(config.get(:orchestration, :bucket_name))
185
+ validator_name = name_for(payload, 'validator.pem')
186
+ if(config.get(:init, :chef, :validator) && bucket.files.get(validator_name).nil?)
187
+ file = bucket.files.build(:name => validator_name)
188
+ file.body = OpenSSL::PKey::RSA.new(2048).export
189
+ file.save
190
+ end
191
+ secret_name = name_for(payload, 'encrypted_data_bag_secret')
192
+ if(config.get(:init, :chef, :encrypted_secret) && bucket.files.get(secret_name).nil?)
193
+ file = bucket.files.build(:name => secret_name)
194
+ file.body = SecureRandom.base64(2048)
195
+ file.save
196
+ end
197
+ end
198
+ end
199
+
200
+ # Store stable asset in object store
201
+ #
202
+ # @param payload [Smash]
203
+ def store_stable_asset(payload, directory)
204
+ if(config.get(:init, :stable))
205
+ ['rm -rf .librarian Gemfile Gemfile.lock', 'librarian-chef install', 'rm -rf tmp'].each do |cmd|
206
+ info "Starting stable store pre-pack command: #{cmd}"
207
+ process_manager.process(payload[:id], cmd) do |process|
208
+ process.cwd = directory
209
+ process.io.inherit!
210
+ process.start
211
+ process.wait
212
+ unless(process.exit_code == 0)
213
+ raise 'Cookbook install failed!'
214
+ end
215
+ end
216
+ info "Completed stable store pre-pack command: #{cmd}"
217
+ end
218
+ debug "Starting stable asset upload for #{payload[:id]}"
219
+ bucket = stacks_api.api_for(:storage).buckets.get(config.get(:orchestration, :bucket_name))
220
+ stable_name = name_for(payload, 'stable.zip')
221
+ file = bucket.files.get(stable_name) || bucket.files.build(:name => stable_name)
222
+ file.body = asset_store.pack(directory)
223
+ file.save
224
+ end
225
+ end
226
+
227
+ # Provide prefixed key name for asset
228
+ #
229
+ # @param payload [Smash]
230
+ # @param asset_name [String
231
+ # @return [String]
232
+ # @note this is currently a no-op and thus are shared across
233
+ # stacks. currently is stubbed for completion of template and
234
+ # interaction logic
235
+ def name_for(payload, asset_name)
236
+ File.join(determine_namespace(payload), asset_name)
237
+ asset_name
238
+ end
239
+
240
+ end
241
+ end
242
+ end
@@ -0,0 +1,4 @@
1
+ require 'jackal-stacks/formatter/webhook'
2
+ require 'jackal-stacks/formatter/code_fetcher'
3
+ require 'jackal-stacks/formatter/github'
4
+ require 'jackal-stacks/formatter/slack'
@@ -0,0 +1,30 @@
1
+ require 'jackal-stacks'
2
+
3
+ module Jackal
4
+ module Stacks
5
+ module Formatter
6
+ # Format code fetcher data for stacks
7
+ class CodeFetcher < Jackal::Formatter
8
+
9
+ SOURCE = 'code_fetcher'
10
+ DESTINATION = 'stacks'
11
+
12
+ # Format payload
13
+ #
14
+ # @param payload [Smash]
15
+ def format(payload)
16
+ if(payload.get(:data, :code_fetcher))
17
+ payload.set(:data, :stacks, :asset,
18
+ payload.get(:data, :code_fetcher, :asset)
19
+ )
20
+ payload.set(:data, :stacks, :reference,
21
+ payload.get(:data, :code_fetcher, :info, :reference)
22
+ )
23
+ end
24
+ end
25
+
26
+ end
27
+
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,39 @@
1
+ require 'jackal-stacks'
2
+
3
+ module Jackal
4
+ module Stacks
5
+ module Formatter
6
+ # Format github data for stacks
7
+ class Github < Jackal::Formatter
8
+
9
+ # Source service
10
+ SOURCE = :github
11
+ # Destination service
12
+ DESTINATION = :stacks
13
+
14
+ # Valid github events
15
+ VALID_EVENTS = %w(create delete push)
16
+
17
+ # Format payload
18
+ #
19
+ # @param payload [Smash]
20
+ def format(payload)
21
+ if(VALID_EVENTS.include?(payload.get(:data, :github, :event)))
22
+ if(payload.get(:data, :github, :query, :template))
23
+ payload.set(:data, :stacks, :template,
24
+ payload.get(:data, :github, :query, :template)
25
+ )
26
+ end
27
+ if(payload.get(:data, :github, :event) == 'delete')
28
+ payload.set(:data, :stacks, :wrecker, true)
29
+ else
30
+ payload.set(:data, :stacks, :builder, true)
31
+ end
32
+ end
33
+ end
34
+
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,43 @@
1
+ require 'jackal-stacks'
2
+
3
+ module Jackal
4
+ module Stacks
5
+ module Formatter
6
+ # Format result for slack notification
7
+ class Slack < Jackal::Formatter
8
+
9
+ # Source service
10
+ SOURCE = 'stacks'
11
+ # Destination service
12
+ DESTINATION = 'slack'
13
+
14
+ NOTIFY_ON = %w(created updated destroyed)
15
+
16
+ # Format payload
17
+ #
18
+ # @param payload [Smash]
19
+ def format(payload)
20
+ if(payload.get(:data, :stacks))
21
+ unless((notify = payload.fetch(:data, :stacks, {}).keys & NOTIFY_ON).empty?)
22
+ msgs = payload.fetch(:data, :slack, :messages, [])
23
+ msgs << Smash.new(
24
+ :description => "Stacks result: #{notify.first}",
25
+ :message => [
26
+ "Stack has been #{notify.first} [name: #{payload.get(:data, :stacks, :name)}]",
27
+ "* Template: #{payload.get(:data, :stacks, :template)}",
28
+ "* Repository: #{payload.get(:data, :code_fetcher, :info, :owner)}/#{payload.get(:data, :code_fetcher, :info, :name)}",
29
+ "* Reference: #{payload.get(:data, :code_fetcher, :info, :reference)}",
30
+ "* SHA: #{payload.get(:data, :code_fetcher, :info, :commit_sha)}"
31
+ ].join("\n"),
32
+ :color => :good
33
+ )
34
+ payload.set(:data, :slack, :messages, msgs)
35
+ end
36
+ end
37
+ end
38
+
39
+ end
40
+
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,27 @@
1
+ require 'jackal-stacks'
2
+
3
+ module Jackal
4
+ module Stacks
5
+ module Formatter
6
+ # Format webhook data for stacks
7
+ class Webhook < Jackal::Formatter
8
+
9
+ SOURCE = 'webhook'
10
+ DESTINATION = 'stacks'
11
+
12
+ # Format payload
13
+ #
14
+ # @param payload [Smash]
15
+ def format(payload)
16
+ if(payload.get(:data, :webhook, :query, :template))
17
+ payload.set(:data, :stacks, :template,
18
+ payload.get(:data, :webhook, :query, :template)
19
+ )
20
+ end
21
+ end
22
+
23
+ end
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,42 @@
1
+ require 'jackal-stacks'
2
+
3
+ module Jackal
4
+ module Stacks
5
+ module StackCommon
6
+
7
+ # @return [Miasma::Models::Orchestration]
8
+ def stacks_api
9
+ memoize(:stacks_api, :direct) do
10
+ Miasma.api(
11
+ config.fetch(
12
+ :orchestration, :api, Smash.new
13
+ ).merge(
14
+ :type => :orchestration
15
+ )
16
+ )
17
+ end
18
+ end
19
+
20
+ # Determine namespace key to use for accessing parameters
21
+ #
22
+ # @param payload [Smash]
23
+ # @return [String]
24
+ # @note if not match found, `default` will return
25
+ def determine_namespace(payload)
26
+ config.fetch(:mappings, Smash.new).map do |ns, glob|
27
+ ns if File.fnmatch?(glob, payload.get(:data, :stacks, :reference).to_s)
28
+ end.compact.first
29
+ end
30
+
31
+ # Generate stack name based on payload
32
+ #
33
+ # @param payload [Smash]
34
+ # @return [String] stack name
35
+ def stack_name(payload)
36
+ s_namespace = determine_namespace(payload)
37
+ "#{s_namespace}-#{payload.get(:data, :stacks, :template).sub(/\.[a-z]+$/, '').gsub(/[^A-Za-z0-9\-]/, '-')}"
38
+ end
39
+
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,6 @@
1
+ module Jackal
2
+ module Stacks
3
+ # Current library version
4
+ VERSION = Gem::Version.new('0.1.0')
5
+ end
6
+ end
@@ -0,0 +1,46 @@
1
+ require 'jackal-stacks'
2
+
3
+ module Jackal
4
+ module Stacks
5
+ # Stack destroyer
6
+ class Wrecker < Callback
7
+
8
+ include StackCommon
9
+
10
+ # Setup callback
11
+ def setup(*_)
12
+ stacks_api
13
+ end
14
+
15
+ # Determine validity of message
16
+ #
17
+ # @param message [Carnivore::Message]
18
+ # @return [Truthy, Falsey]
19
+ def valid?(message)
20
+ super do |payload|
21
+ payload.get(:data, :stacks, :wrecker)
22
+ end
23
+ end
24
+
25
+ # Build or update stacks
26
+ #
27
+ # @param message [Carnivore::Message]
28
+ def execute(message)
29
+ failure_wrap(message) do |payload|
30
+ s_name = stack_name(payload)
31
+ stack = stacks_api.stacks.get(s_name)
32
+ if(stack)
33
+ info "Stack currently exists. Destroying. [#{stack.name}]"
34
+ stack.destroy
35
+ payload.set(:data, :stacks, :destroyed, true)
36
+ job_completed(:stacks, payload, message)
37
+ else
38
+ error "Failed to locate requested stack for destruction [#{s_name}]"
39
+ failed(payload, message, 'Requested stack does not exist')
40
+ end
41
+ end
42
+ end
43
+
44
+ end
45
+ end
46
+ end
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jackal-stacks
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Chris Roberts
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-03-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: jackal
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: librarian-chef
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: sfn
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: Build stuff!
56
+ email: code@chrisroberts.org
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - CHANGELOG.md
62
+ - CONTRIBUTING.md
63
+ - LICENSE
64
+ - README.md
65
+ - jackal-stacks.gemspec
66
+ - lib/jackal-stacks.rb
67
+ - lib/jackal-stacks/builder.rb
68
+ - lib/jackal-stacks/formatter.rb
69
+ - lib/jackal-stacks/formatter/code_fetcher.rb
70
+ - lib/jackal-stacks/formatter/github.rb
71
+ - lib/jackal-stacks/formatter/slack.rb
72
+ - lib/jackal-stacks/formatter/webhook.rb
73
+ - lib/jackal-stacks/stack_common.rb
74
+ - lib/jackal-stacks/version.rb
75
+ - lib/jackal-stacks/wrecker.rb
76
+ homepage: https://github.com/carnivore-rb/jackal-stacks
77
+ licenses:
78
+ - Apache 2.0
79
+ metadata: {}
80
+ post_install_message:
81
+ rdoc_options: []
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ requirements: []
95
+ rubyforge_project:
96
+ rubygems_version: 2.2.2
97
+ signing_key:
98
+ specification_version: 4
99
+ summary: Message processing helper
100
+ test_files: []
101
+ has_rdoc: