jackal-stacks 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: