jackal-cfn 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md ADDED
File without changes
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-cfn/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-cfn/issues
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2014 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,26 @@
1
+ # Jackal CFN
2
+
3
+ CFN proxy to the jackal subsystem.
4
+
5
+ ## Supported payload generation
6
+
7
+ ### Events
8
+
9
+ Stack events are injected with the following
10
+ payload structure:
11
+
12
+ ```json
13
+ ```
14
+
15
+ ### Resources
16
+
17
+ Custom stack resources are injected with the
18
+ following structure:
19
+
20
+ ```json
21
+ ```
22
+
23
+ ## Info
24
+
25
+ * Repository: https://github.com/carnviore-rb/jackal-cfn
26
+ * IRC: Freenode @ #carnivore
@@ -0,0 +1,16 @@
1
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__)) + '/lib/'
2
+ require 'jackal-cfn/version'
3
+ Gem::Specification.new do |s|
4
+ s.name = 'jackal-cfn'
5
+ s.version = Jackal::Cfn::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-cfn'
10
+ s.description = 'CFN callback helpers'
11
+ s.require_path = 'lib'
12
+ s.license = 'Apache 2.0'
13
+ s.add_dependency 'jackal'
14
+ s.add_dependency 'patron'
15
+ s.files = Dir['lib/**/*'] + %w(jackal-cfn.gemspec README.md CHANGELOG.md CONTRIBUTING.md LICENSE)
16
+ end
data/lib/jackal-cfn.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'jackal'
2
+ require 'jackal-cfn/version'
3
+
4
+ module Jackal
5
+ # Cfn entry system
6
+ module Cfn
7
+ autoload :Event, 'jackal-cfn/event'
8
+ autoload :Resource, 'jackal-cfn/resource'
9
+ end
10
+ end
@@ -0,0 +1,76 @@
1
+ require 'jackal-cfn'
2
+
3
+ module Jackal
4
+ module Cfn
5
+ # Callback for event types
6
+ class Event < Jackal::Callback
7
+
8
+ # Unpack message and create payload
9
+ #
10
+ # @param message [Carnivore::Message]
11
+ # @return [Smash]
12
+ def unpack(message)
13
+ payload = super
14
+ payload = format_event(payload.fetch('Body', 'Message', payload))
15
+ payload[:origin_type] = message[:message].get('Body', 'Type')
16
+ payload[:origin_subject] = message[:message].get('Body', 'Subject')
17
+ end
18
+
19
+ # Determine message validity
20
+ #
21
+ # @param message [Carnivore::Message]
22
+ # @return [TrueClass, FalseClass]
23
+ def valid?(message)
24
+ super do |payload|
25
+ result = payload[:origin_type] == 'Notification' &&
26
+ payload[:origin_subject].downcase.include?('cloudformation notification')
27
+ if(result && block_given?)
28
+ yield payload
29
+ else
30
+ result
31
+ end
32
+ end
33
+ end
34
+
35
+ # Format payload into proper event structure
36
+ #
37
+ # @param evt [Hash]
38
+ # @return [Smash]
39
+ def format_event(evt)
40
+ parts = evt.split("\n").map do |entry|
41
+ chunks = entry.split('=')
42
+ key = parts.unshift.strip
43
+ value = parts.join.strip.sub(/^'/, '').sub(/'$/, '').strip
44
+ [key, value]
45
+ end
46
+ event = Smash[parts]
47
+ unless(event['ResourceProperties'].to_s.empty?)
48
+ begin
49
+ event['ResourceProperties'] = MultiJson.load(event['ResourceProperties'])
50
+ rescue MultiJson::LoadError => e
51
+ error "Failed to load `ResourceProperties`: #{e.class} - #{e}"
52
+ debug "#{e.class}: #{e}\n#{e.backtrace.join("\n")}"
53
+ end
54
+ else
55
+ event['ResourceProperties'] = {}
56
+ end
57
+ Smash.new(Carnivore::Utils.symbolize_hash(event))
58
+ end
59
+
60
+ # Generate payload and drop
61
+ #
62
+ # @param message [Carnivore::Message]
63
+ def execute(message)
64
+ failure_wrap do |payload|
65
+ job_completed(
66
+ new_payload(
67
+ config[:name],
68
+ :cfn_event => payload
69
+ )
70
+ )
71
+ end
72
+ end
73
+
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,144 @@
1
+ require 'jackal-cfn'
2
+
3
+ module Jackal
4
+ module Cfn
5
+ # Callback for resource types
6
+ class Resource < Jackal::Callback
7
+
8
+ VALID_RESOURCE_STATUS = ['SUCCESS', 'FAILED']
9
+
10
+ autoload :HashExtractor, 'jackal-cfn/resource/hash_extractor'
11
+
12
+ # Physical ID of the resource created
13
+ #
14
+ # @return [String]
15
+ # @note this should be overridden in subclasses when actual
16
+ # resources are being created
17
+ def physical_resource_id
18
+ "#{self.class.name}-#{Celluloid.uuid}"
19
+ end
20
+
21
+ # Generate response hash
22
+ #
23
+ # @param payload [Hash]
24
+ # @return [Hash] default response content
25
+ def build_response(payload)
26
+ args = transform_parameters(payload)
27
+ Smash.new(
28
+ 'LogicalResourceId' => args[:logical_resource_id],
29
+ 'PhysicalResourceId' => args.fetch(:physical_resource_id, physical_resource_id),
30
+ 'StackId' => args[:stack_id],
31
+ 'Status' => 'SUCCESS',
32
+ 'Reason' => nil,
33
+ 'Data' => Smash.new
34
+ )
35
+ end
36
+
37
+ # Provide remote endpoint session for sending response
38
+ #
39
+ # @param host [String] end point host
40
+ # @param scheme [String] end point scheme
41
+ # @return [Patron::Session]
42
+ def response_endpoint(host, scheme)
43
+ session = Patron::Session.new
44
+ session.timeout = config.fetch(:response_timeout, 20)
45
+ session.connect_timeout = config.fetch(:connection_timeout, 10)
46
+ session.base_url = "#{scheme}://#{host}"
47
+ session.headers['User-Agent'] = "JackalCfn/#{Jackal::Cfn::VERSION.version}"
48
+ session
49
+ end
50
+
51
+ # Send response to the waiting stack
52
+ #
53
+ # @param response [Hash]
54
+ # @param response_url [String] response endpoint
55
+ # @return [TrueClass, FalseClass]
56
+ def respond_to_stack(response, response_url)
57
+ unless(VALID_RESOURCE_STATUS.include?(response['Status']))
58
+ raise ArgumentError.new "Invalid resource status provided. Got: #{response['Status']}. Allowed: #{VALID_RESOURCE_STATUS.join(', ')}"
59
+ end
60
+ if(response['Status'] == 'FAILED' && !response['Reason'])
61
+ response['Reason'] = 'Unknown'
62
+ end
63
+ url = URI.parse(response_url)
64
+ connection = response_endpoint(url.host, url.scheme)
65
+ path = "#{url.path}?#{url.query}"
66
+ debug "Custom resource response data: #{response.inspect}"
67
+ complete = connection.request(:put, path, {}, :data => JSON.dump(response))
68
+ case complete.status
69
+ when 200
70
+ info "Custom resource response complete! (Sent to: #{url})"
71
+ true
72
+ when 403
73
+ error "Custom resource response failed. Endpoint is forbidden (403): #{url}"
74
+ false
75
+ when 404
76
+ error "Custom resource response failed. Endpoint is not found (404): #{url}"
77
+ false
78
+ else
79
+ raise "Response failed. Received status: #{complete.status} endpoint: #{url}"
80
+ end
81
+ end
82
+
83
+ # Unpack message and create payload
84
+ #
85
+ # @param message [Carnivore::Message]
86
+ # @return [Smash]
87
+ def unpack(message)
88
+ payload = super
89
+ payload = Smash.new(
90
+ MultiJson.load(
91
+ payload.fetch('Body', 'Message', payload)
92
+ )
93
+ )
94
+ payload[:origin_type] = message[:message].get('Body', 'Type')
95
+ payload[:origin_subject] = message[:message].get('Body', 'Subject')
96
+ payload
97
+ end
98
+
99
+ # Determine message validity
100
+ #
101
+ # @param message [Carnivore::Message]
102
+ # @return [TrueClass, FalseClass]
103
+ def valid?(message)
104
+ super do |payload|
105
+ result = payload[:origin_type] == 'Notification' &&
106
+ payload[:origin_subject].downcase.include?('cloudformation custom resource')
107
+ if(result && block_given?)
108
+ yield payload
109
+ else
110
+ result
111
+ end
112
+ end
113
+ end
114
+
115
+ # Generate payload and drop
116
+ #
117
+ # @param message [Carnivore::Message]
118
+ def execute(message)
119
+ failure_wrap(message) do |payload|
120
+ job_completed(
121
+ new_payload(
122
+ config[:name],
123
+ :cfn_resource => payload
124
+ )
125
+ )
126
+ end
127
+ end
128
+
129
+ # Snake case top level keys in hash
130
+ #
131
+ # @param params [Hash]
132
+ # @return [Hash] new hash with snake cased toplevel keys
133
+ def transform_parameters(params)
134
+ Smash.new.tap do |new_hash|
135
+ params.each do |key, value|
136
+ key = key.gsub(/(?<![A-Z])([A-Z])/, '_\1').sub(/^_/, '').downcase.to_sym
137
+ new_hash[key] = value
138
+ end
139
+ end
140
+ end
141
+
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,66 @@
1
+ require 'jackal-cfn'
2
+
3
+ module Jackal
4
+ module Cfn
5
+ class Resource
6
+ # Extract value from hash
7
+ #
8
+ # Expected resource properties:
9
+ # {
10
+ # "Properties": {
11
+ # "Parameters": {
12
+ # "Key": "path.to.value.in.hash",
13
+ # "Value": Hash_or_JSON_string
14
+ # }
15
+ # }
16
+ # }
17
+ class HashExtractor < Resource
18
+
19
+ # Setup the dependency requirements for the callback
20
+ def setup(*_)
21
+ require 'patron'
22
+ end
23
+
24
+ # Validity of message
25
+ #
26
+ # @param message [Carnivore::Message]
27
+ # @return [Truthy, Falsey]
28
+ def valid?(message)
29
+ super do |payload|
30
+ payload.get('ResourceProperties', 'Action') == 'hash_extractor'
31
+ end
32
+ end
33
+
34
+ # Process message, send value back to CFN
35
+ #
36
+ # @param message [Carnivore::Message]
37
+ def execute(message)
38
+ payload = transform_parameters(unpack(message))
39
+ debug "Processing payload: #{payload.inspect}"
40
+ properties = transform_parameters(payload[:resource_properties])
41
+ cfn_response = build_response(payload)
42
+ parameters = transform_parameters(properties[:parameters])
43
+ key = parameters[:key].split('.')
44
+ value = parameters[:value]
45
+ if(value.is_a?(String))
46
+ value = MultiJson.load(value).to_smash
47
+ end
48
+ return_value = value.get(*key)
49
+ if(return_value.is_a?(Enumerable))
50
+ return_value = MultiJson.dump(return_value)
51
+ end
52
+ cfn_response['Data']['Payload'] = return_value
53
+ respond_to_stack(cfn_response, payload[:response_url])
54
+ completed(
55
+ new_payload(
56
+ config.fetch(:name, :hash_extractor),
57
+ :cfn_resource => payload
58
+ ),
59
+ message
60
+ )
61
+ end
62
+
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,6 @@
1
+ module Jackal
2
+ module Cfn
3
+ # Current version
4
+ VERSION = Gem::Version.new('0.1.0')
5
+ end
6
+ end
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jackal-cfn
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Chris Roberts
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-07-03 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: jackal
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: patron
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description: CFN callback helpers
47
+ email: code@chrisroberts.org
48
+ executables: []
49
+ extensions: []
50
+ extra_rdoc_files: []
51
+ files:
52
+ - lib/jackal-cfn.rb
53
+ - lib/jackal-cfn/event.rb
54
+ - lib/jackal-cfn/version.rb
55
+ - lib/jackal-cfn/resource.rb
56
+ - lib/jackal-cfn/resource/hash_extractor.rb
57
+ - jackal-cfn.gemspec
58
+ - README.md
59
+ - CHANGELOG.md
60
+ - CONTRIBUTING.md
61
+ - LICENSE
62
+ homepage: https://github.com/carnivore-rb/jackal-cfn
63
+ licenses:
64
+ - Apache 2.0
65
+ post_install_message:
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ none: false
71
+ requirements:
72
+ - - ! '>='
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ! '>='
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ requirements: []
82
+ rubyforge_project:
83
+ rubygems_version: 1.8.24
84
+ signing_key:
85
+ specification_version: 3
86
+ summary: Message processing helper
87
+ test_files: []
88
+ has_rdoc: