jackal-cfn 0.1.4 → 0.1.6
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 +7 -0
- data/CHANGELOG.md +5 -0
- data/lib/jackal-cfn/event.rb +63 -16
- data/lib/jackal-cfn/resource/ami_manager.rb +90 -88
- data/lib/jackal-cfn/resource/hash_extractor.rb +28 -34
- data/lib/jackal-cfn/resource.rb +94 -79
- data/lib/jackal-cfn/utils/fog.rb +78 -0
- data/lib/jackal-cfn/utils/http.rb +29 -0
- data/lib/jackal-cfn/utils.rb +33 -0
- data/lib/jackal-cfn/version.rb +1 -1
- data/lib/jackal-cfn.rb +7 -0
- metadata +21 -24
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: 4082d5d4a5132d52521ed5904e2f94aa18602848
|
|
4
|
+
data.tar.gz: d04357c2f76bf711546f6013dac469f2961842f9
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 9b2206efcf53158bc40e26ab9ddb86152d815c2981f6a3fbed99a4b0ed5df2b965879652d0f069dec0b2b1c7dfa1f2b1fa6acf08e552a673599981957f952bb8
|
|
7
|
+
data.tar.gz: c209041a994159fa81d27be044178fa6ee90b2dc52de92d808652bf1670fcebb8cdb1a258dd2a38769793c04bf2c8819940c722bebdf41e97d74b6f570731e31
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
# v0.1.6
|
|
2
|
+
* Isolate event and resource to simple handle format and forward
|
|
3
|
+
* Move builtin custom resources within new module `CfnTools`
|
|
4
|
+
* Update `Resource` and `Event` to subclass gracefully
|
|
5
|
+
|
|
1
6
|
# v0.1.4
|
|
2
7
|
* Include ami manager resource (handle deletion of amis only for now)
|
|
3
8
|
|
data/lib/jackal-cfn/event.rb
CHANGED
|
@@ -5,15 +5,58 @@ module Jackal
|
|
|
5
5
|
# Callback for event types
|
|
6
6
|
class Event < Jackal::Callback
|
|
7
7
|
|
|
8
|
+
module InheritedValidity
|
|
9
|
+
# Determine message validity
|
|
10
|
+
#
|
|
11
|
+
# @param message [Carnivore::Message]
|
|
12
|
+
# @return [TrueClass, FalseClass]
|
|
13
|
+
def valid?(message)
|
|
14
|
+
super do |payload|
|
|
15
|
+
data = payload.fetch(:data, :cfn_event, Smash.new)
|
|
16
|
+
result = data[:origin_type] == 'Notification' &&
|
|
17
|
+
data[:origin_subject].to_s.downcase.include?('cloudformation notification')
|
|
18
|
+
if(result && block_given?)
|
|
19
|
+
yield payload
|
|
20
|
+
else
|
|
21
|
+
result
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
include Jackal::Cfn::Utils
|
|
29
|
+
include Jackal::Cfn::Utils::Http
|
|
30
|
+
|
|
31
|
+
# Update validity checks in subclasses
|
|
32
|
+
#
|
|
33
|
+
# @param klass [Class]
|
|
34
|
+
def self.inherited(klass)
|
|
35
|
+
klass.class_eval do
|
|
36
|
+
include InheritedValidity
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
8
40
|
# Unpack message and create payload
|
|
9
41
|
#
|
|
10
42
|
# @param message [Carnivore::Message]
|
|
11
43
|
# @return [Smash]
|
|
12
44
|
def unpack(message)
|
|
13
45
|
payload = super
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
46
|
+
if(self.class == Jackal::Cfn::Event)
|
|
47
|
+
begin
|
|
48
|
+
unless(payload.get(:data, :cfn_event))
|
|
49
|
+
payload = format_event(payload.fetch('Body', 'Message', payload))
|
|
50
|
+
payload[:origin_type] = message[:message].get('Body', 'Type')
|
|
51
|
+
payload[:origin_subject] = message[:message].get('Body', 'Subject')
|
|
52
|
+
end
|
|
53
|
+
payload
|
|
54
|
+
rescue
|
|
55
|
+
Smash.new
|
|
56
|
+
end
|
|
57
|
+
else
|
|
58
|
+
payload.to_smash.fetch('Body', payload.to_smash)
|
|
59
|
+
end
|
|
17
60
|
end
|
|
18
61
|
|
|
19
62
|
# Determine message validity
|
|
@@ -22,12 +65,11 @@ module Jackal
|
|
|
22
65
|
# @return [TrueClass, FalseClass]
|
|
23
66
|
def valid?(message)
|
|
24
67
|
super do |payload|
|
|
25
|
-
|
|
26
|
-
payload[:origin_subject].downcase.include?('cloudformation notification')
|
|
27
|
-
if(result && block_given?)
|
|
68
|
+
if(block_given?)
|
|
28
69
|
yield payload
|
|
29
70
|
else
|
|
30
|
-
|
|
71
|
+
payload[:origin_type] == 'Notification' &&
|
|
72
|
+
payload[:origin_subject].downcase.include?('cloudformation notification')
|
|
31
73
|
end
|
|
32
74
|
end
|
|
33
75
|
end
|
|
@@ -39,8 +81,8 @@ module Jackal
|
|
|
39
81
|
def format_event(evt)
|
|
40
82
|
parts = evt.split("\n").map do |entry|
|
|
41
83
|
chunks = entry.split('=')
|
|
42
|
-
key =
|
|
43
|
-
value =
|
|
84
|
+
key = chunks.shift.strip
|
|
85
|
+
value = chunks.join.strip.sub(/^'/, '').sub(/'$/, '').strip
|
|
44
86
|
[key, value]
|
|
45
87
|
end
|
|
46
88
|
event = Smash[parts]
|
|
@@ -61,13 +103,18 @@ module Jackal
|
|
|
61
103
|
#
|
|
62
104
|
# @param message [Carnivore::Message]
|
|
63
105
|
def execute(message)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
106
|
+
data_payload = unpack(message)
|
|
107
|
+
payload = new_payload(
|
|
108
|
+
config[:name],
|
|
109
|
+
:cfn_event => data_payload
|
|
110
|
+
)
|
|
111
|
+
if(config[:reprocess])
|
|
112
|
+
my_input = "#{source_prefix}_input"
|
|
113
|
+
debug "Reprocessing payload through current source (#{my_input})"
|
|
114
|
+
Carnivore::Supervisor.supervisor[my_input].transmit(payload)
|
|
115
|
+
message.confirm!
|
|
116
|
+
else
|
|
117
|
+
completed(payload, message)
|
|
71
118
|
end
|
|
72
119
|
end
|
|
73
120
|
|
|
@@ -1,109 +1,111 @@
|
|
|
1
1
|
require 'jackal-cfn'
|
|
2
2
|
|
|
3
3
|
module Jackal
|
|
4
|
-
module
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
4
|
+
module CfnTools
|
|
5
|
+
# Manage AMI Resources
|
|
6
|
+
#
|
|
7
|
+
# Expected resource:
|
|
8
|
+
# {
|
|
9
|
+
# "Type": "Custom::AmiManager",
|
|
10
|
+
# "Properties": {
|
|
11
|
+
# "Parameters": {
|
|
12
|
+
# "InstanceId": "",
|
|
13
|
+
# "AmiId": "",
|
|
14
|
+
# "Action": "",
|
|
15
|
+
# "Region": ""
|
|
16
|
+
# }
|
|
17
|
+
# }
|
|
18
|
+
# }
|
|
19
|
+
#
|
|
20
|
+
# Required configuration:
|
|
21
|
+
# {
|
|
22
|
+
# "config": {
|
|
23
|
+
# "ami": {
|
|
24
|
+
# "credentials": {
|
|
25
|
+
# "compute": {
|
|
26
|
+
# FOG_CREDENTIALS
|
|
27
|
+
# }
|
|
28
|
+
# }
|
|
29
|
+
# }
|
|
30
|
+
# }
|
|
31
|
+
# }
|
|
32
|
+
class AmiManager < Jackal::Cfn::Resource
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
PHYSICAL_ID_JOINER = '__-__'
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
36
|
+
# Process message, send value back to CFN
|
|
37
|
+
#
|
|
38
|
+
# @param message [Carnivore::Message]
|
|
39
|
+
def execute(message)
|
|
40
|
+
failure_wrap do |payload|
|
|
41
|
+
cfn_resource = rekey_hash(payload.get(:data, :cfn_resource))
|
|
42
|
+
properties = rekey_hash(cfn_resource[:resource_properties])
|
|
43
|
+
parameters = rekey_hash(properties[:parameters])
|
|
44
|
+
cfn_response = build_response(cfn_resource)
|
|
45
|
+
case cfn_resource[:request_type]
|
|
44
46
|
when :create
|
|
45
|
-
ami_response_update(
|
|
47
|
+
ami_response_update(cfn_response, parameters)
|
|
46
48
|
when :delete
|
|
47
|
-
destroy_ami(
|
|
49
|
+
destroy_ami(cfn_response, cfn_resource, parameters)
|
|
48
50
|
when :update
|
|
49
51
|
else
|
|
50
|
-
error "Unknown request type received: #{
|
|
52
|
+
error "Unknown request type received: #{cfn_resource[:request_type].inspect}"
|
|
51
53
|
response['Status'] = 'FAILED'
|
|
52
54
|
response['Reason'] = 'Unknown request type received'
|
|
53
55
|
end
|
|
54
|
-
respond_to_stack(
|
|
55
|
-
completed(
|
|
56
|
-
new_payload(
|
|
57
|
-
config.fetch(:name, :ami_manager),
|
|
58
|
-
:cfn_resource => payload
|
|
59
|
-
),
|
|
60
|
-
message
|
|
61
|
-
)
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
# Update the physical resource id to include the ami id
|
|
65
|
-
#
|
|
66
|
-
# @param response [Hash] cfn response
|
|
67
|
-
# @param parameters [Hash] resource parameters
|
|
68
|
-
# @return [Hash] updated response hash
|
|
69
|
-
def ami_response_update(response, parameters)
|
|
70
|
-
response['PhysicalResourceId'] = [
|
|
71
|
-
response['PhysicalResourceId'],
|
|
72
|
-
parameters[:ami_id]
|
|
73
|
-
].join(PHYSICAL_ID_JOINER)
|
|
74
|
-
response
|
|
56
|
+
respond_to_stack(cfn_response, cfn_resource[:response_url])
|
|
57
|
+
completed(payload, message)
|
|
75
58
|
end
|
|
59
|
+
end
|
|
76
60
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
response['Reason'] = e.message
|
|
90
|
-
rescue => e
|
|
91
|
-
error "Failed to remove AMI: #{e.class}: #{e}"
|
|
92
|
-
response['Status'] = 'FAILED'
|
|
93
|
-
response['Reason'] = e.message
|
|
94
|
-
end
|
|
95
|
-
true
|
|
96
|
-
end
|
|
61
|
+
# Update the physical resource id to include the ami id
|
|
62
|
+
#
|
|
63
|
+
# @param response [Hash] cfn response
|
|
64
|
+
# @param parameters [Hash] resource parameters
|
|
65
|
+
# @return [Hash] updated response hash
|
|
66
|
+
def ami_response_update(response, parameters)
|
|
67
|
+
response['PhysicalResourceId'] = [
|
|
68
|
+
response['PhysicalResourceId'],
|
|
69
|
+
parameters[:ami_id]
|
|
70
|
+
].join(PHYSICAL_ID_JOINER)
|
|
71
|
+
response
|
|
72
|
+
end
|
|
97
73
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
74
|
+
# Destroy the AMI referenced by the resource
|
|
75
|
+
#
|
|
76
|
+
# @param response [Hash] cfn response
|
|
77
|
+
# @param payload [Hash] message payload
|
|
78
|
+
# @param parameters [Hash] resource parameters
|
|
79
|
+
# @return [TrueClass]
|
|
80
|
+
def destroy_ami(response, payload, parameters)
|
|
81
|
+
ami_id = payload[:physical_resource_id].split(PHYSICAL_ID_JOINER).last
|
|
82
|
+
begin
|
|
83
|
+
compute_api(parameters[:region]).deregister_image(ami_id)
|
|
84
|
+
rescue Fog::Compute::AWS::Error => e
|
|
85
|
+
warn "Non-fatal error encountered on AMI removal: #{e.class}: #{e}"
|
|
86
|
+
response['Reason'] = e.message
|
|
87
|
+
rescue => e
|
|
88
|
+
error "Failed to remove AMI: #{e.class}: #{e}"
|
|
89
|
+
response['Status'] = 'FAILED'
|
|
90
|
+
response['Reason'] = e.message
|
|
104
91
|
end
|
|
92
|
+
true
|
|
93
|
+
end
|
|
105
94
|
|
|
95
|
+
# Build new compute api connection
|
|
96
|
+
#
|
|
97
|
+
# @param region [String] AWS region ami exists
|
|
98
|
+
# @return [Fog::Compute]
|
|
99
|
+
def compute_api(region)
|
|
100
|
+
Fog::Compute.new(
|
|
101
|
+
{:provider => :aws}.merge(
|
|
102
|
+
config.fetch(:ami, :credentials, :compute).merge(
|
|
103
|
+
:region => region
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
)
|
|
106
107
|
end
|
|
108
|
+
|
|
107
109
|
end
|
|
108
110
|
end
|
|
109
111
|
end
|
|
@@ -1,52 +1,46 @@
|
|
|
1
1
|
require 'jackal-cfn'
|
|
2
2
|
|
|
3
3
|
module Jackal
|
|
4
|
-
module
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class HashExtractor < Resource
|
|
4
|
+
module CfnTools
|
|
5
|
+
# Extract value from hash
|
|
6
|
+
#
|
|
7
|
+
# Expected resource:
|
|
8
|
+
# {
|
|
9
|
+
# "Type": "Custom::HashExtractor",
|
|
10
|
+
# "Properties": {
|
|
11
|
+
# "Parameters": {
|
|
12
|
+
# "Key": "path.to.value.in.hash",
|
|
13
|
+
# "Value": Hash_or_JSON_string
|
|
14
|
+
# }
|
|
15
|
+
# }
|
|
16
|
+
# }
|
|
17
|
+
class HashExtractor < Jackal::Cfn::Resource
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
properties =
|
|
27
|
-
|
|
28
|
-
|
|
19
|
+
# Process message, send value back to CFN
|
|
20
|
+
#
|
|
21
|
+
# @param message [Carnivore::Message]
|
|
22
|
+
def execute(message)
|
|
23
|
+
failure_wrap do |payload|
|
|
24
|
+
cfn_resource = rekey_hash(payload.get(:data, :cfn_resource))
|
|
25
|
+
properties = rekey_hash(cfn_resource[:resource_properties])
|
|
26
|
+
parameters = rekey_hash(properties[:parameters])
|
|
27
|
+
cfn_response = build_response(cfn_resource)
|
|
29
28
|
key = parameters[:key].split('.')
|
|
30
29
|
value = parameters[:value]
|
|
31
|
-
|
|
32
|
-
value
|
|
30
|
+
unless(value.is_a?(String))
|
|
31
|
+
raise TypeError.new("Expecting `String` value but received `#{value.class}`")
|
|
33
32
|
end
|
|
33
|
+
value = MultiJson.load(value).to_smash
|
|
34
34
|
return_value = value.get(*key)
|
|
35
35
|
if(return_value.is_a?(Enumerable))
|
|
36
36
|
return_value = MultiJson.dump(return_value)
|
|
37
37
|
end
|
|
38
38
|
cfn_response['Data']['Payload'] = return_value
|
|
39
39
|
respond_to_stack(cfn_response, payload[:response_url])
|
|
40
|
-
completed(
|
|
41
|
-
new_payload(
|
|
42
|
-
config.fetch(:name, :hash_extractor),
|
|
43
|
-
:cfn_resource => payload
|
|
44
|
-
),
|
|
45
|
-
message
|
|
46
|
-
)
|
|
40
|
+
completed(payload, message)
|
|
47
41
|
end
|
|
48
|
-
|
|
49
42
|
end
|
|
43
|
+
|
|
50
44
|
end
|
|
51
45
|
end
|
|
52
46
|
end
|
data/lib/jackal-cfn/resource.rb
CHANGED
|
@@ -5,10 +5,58 @@ module Jackal
|
|
|
5
5
|
# Callback for resource types
|
|
6
6
|
class Resource < Jackal::Callback
|
|
7
7
|
|
|
8
|
+
# Validity method for subclasses
|
|
9
|
+
module InheritedValidity
|
|
10
|
+
|
|
11
|
+
# Determine message validity
|
|
12
|
+
#
|
|
13
|
+
# @param message [Carnivore::Message]
|
|
14
|
+
# @return [TrueClass, FalseClass]
|
|
15
|
+
def valid?(message)
|
|
16
|
+
super do |payload|
|
|
17
|
+
data = payload.fetch(:data, :cfn_resource, Smash.new)
|
|
18
|
+
resource_type = data[:resource_type].to_s.split('::').last
|
|
19
|
+
result = data[:origin_type] == 'Notification' &&
|
|
20
|
+
data[:origin_subject].to_s.downcase.include?('cloudformation custom resource') &&
|
|
21
|
+
self.class.to_s.split('::').last == resource_type
|
|
22
|
+
if(result && block_given?)
|
|
23
|
+
yield payload
|
|
24
|
+
else
|
|
25
|
+
result
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
include Jackal::Cfn::Utils
|
|
33
|
+
include Jackal::Cfn::Utils::Http
|
|
34
|
+
|
|
8
35
|
VALID_RESOURCE_STATUS = ['SUCCESS', 'FAILED']
|
|
9
36
|
|
|
10
|
-
|
|
11
|
-
|
|
37
|
+
# Update validity checks in subclasses
|
|
38
|
+
#
|
|
39
|
+
# @param klass [Class]
|
|
40
|
+
def self.inherited(klass)
|
|
41
|
+
klass.class_eval do
|
|
42
|
+
include InheritedValidity
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Determine message validity
|
|
47
|
+
#
|
|
48
|
+
# @param message [Carnivore::Message]
|
|
49
|
+
# @return [TrueClass, FalseClass]
|
|
50
|
+
def valid?(message)
|
|
51
|
+
super do |payload|
|
|
52
|
+
if(block_given?)
|
|
53
|
+
yield payload
|
|
54
|
+
else
|
|
55
|
+
payload[:origin_type] == 'Notification' &&
|
|
56
|
+
payload[:origin_subject].to_s.downcase.include?('cloudformation custom resource')
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
12
60
|
|
|
13
61
|
# Setup the dependency requirements for the callback
|
|
14
62
|
def setup(*_)
|
|
@@ -21,40 +69,30 @@ module Jackal
|
|
|
21
69
|
# @note this should be overridden in subclasses when actual
|
|
22
70
|
# resources are being created
|
|
23
71
|
def physical_resource_id
|
|
24
|
-
"#{self.class.name}-#{Celluloid.uuid}"
|
|
72
|
+
"#{self.class.name.split('::').last}-#{Celluloid.uuid}"
|
|
25
73
|
end
|
|
26
74
|
|
|
27
75
|
# Generate response hash
|
|
28
76
|
#
|
|
29
|
-
# @param
|
|
77
|
+
# @param cfn_resource [Hash]
|
|
78
|
+
# @option cfn_resource [String] :logical_resource_id
|
|
79
|
+
# @option cfn_resource [String] :physical_resource_id
|
|
80
|
+
# @option cfn_resource [String] :stack_id
|
|
81
|
+
# @option cfn_resource [String] :request_id
|
|
30
82
|
# @return [Hash] default response content
|
|
31
|
-
def build_response(
|
|
32
|
-
args = transform_parameters(payload)
|
|
83
|
+
def build_response(cfn_resource)
|
|
33
84
|
Smash.new(
|
|
34
|
-
'LogicalResourceId' =>
|
|
35
|
-
'PhysicalResourceId' =>
|
|
36
|
-
'StackId' =>
|
|
37
|
-
'RequestId' =>
|
|
85
|
+
'LogicalResourceId' => cfn_resource[:logical_resource_id],
|
|
86
|
+
'PhysicalResourceId' => cfn_resource.fetch(:physical_resource_id, physical_resource_id),
|
|
87
|
+
'StackId' => cfn_resource[:stack_id],
|
|
88
|
+
'RequestId' => cfn_resource[:request_id],
|
|
38
89
|
'Status' => 'SUCCESS',
|
|
39
|
-
'
|
|
40
|
-
|
|
90
|
+
'Data' => Smash.new(
|
|
91
|
+
'Reason' => 'None'
|
|
92
|
+
)
|
|
41
93
|
)
|
|
42
94
|
end
|
|
43
95
|
|
|
44
|
-
# Provide remote endpoint session for sending response
|
|
45
|
-
#
|
|
46
|
-
# @param host [String] end point host
|
|
47
|
-
# @param scheme [String] end point scheme
|
|
48
|
-
# @return [Patron::Session]
|
|
49
|
-
def response_endpoint(host, scheme)
|
|
50
|
-
session = Patron::Session.new
|
|
51
|
-
session.timeout = config.fetch(:response_timeout, 20)
|
|
52
|
-
session.connect_timeout = config.fetch(:connection_timeout, 10)
|
|
53
|
-
session.base_url = "#{scheme}://#{host}"
|
|
54
|
-
session.headers['User-Agent'] = "JackalCfn/#{Jackal::Cfn::VERSION.version}"
|
|
55
|
-
session
|
|
56
|
-
end
|
|
57
|
-
|
|
58
96
|
# Send response to the waiting stack
|
|
59
97
|
#
|
|
60
98
|
# @param response [Hash]
|
|
@@ -93,33 +131,26 @@ module Jackal
|
|
|
93
131
|
# @return [Smash]
|
|
94
132
|
def unpack(message)
|
|
95
133
|
payload = super
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
payload
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
super do |payload|
|
|
114
|
-
resource_type = payload[:resource_type].split('::').last
|
|
115
|
-
result = payload[:origin_type] == 'Notification' &&
|
|
116
|
-
payload[:origin_subject].downcase.include?('cloudformation custom resource') &&
|
|
117
|
-
resource_type == self.class.name.split('::').last
|
|
118
|
-
if(result && block_given?)
|
|
119
|
-
yield payload
|
|
120
|
-
else
|
|
121
|
-
result
|
|
134
|
+
if(self.class == Jackal::Cfn::Resource)
|
|
135
|
+
begin
|
|
136
|
+
if(payload['Body'])
|
|
137
|
+
payload = Smash.new(
|
|
138
|
+
MultiJson.load(
|
|
139
|
+
payload.fetch('Body', 'Message', payload)
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
payload = transform_parameters(payload)
|
|
143
|
+
payload[:origin_type] = message[:message].get('Body', 'Type')
|
|
144
|
+
payload[:origin_subject] = message[:message].get('Body', 'Subject')
|
|
145
|
+
payload[:request_type] = snakecase(payload[:request_type])
|
|
146
|
+
end
|
|
147
|
+
payload
|
|
148
|
+
rescue MultiJson::ParseError
|
|
149
|
+
# Not our expected format so return empty payload
|
|
150
|
+
Smash.new
|
|
122
151
|
end
|
|
152
|
+
else
|
|
153
|
+
payload.to_smash.fetch('Body', payload.to_smash)
|
|
123
154
|
end
|
|
124
155
|
end
|
|
125
156
|
|
|
@@ -127,36 +158,20 @@ module Jackal
|
|
|
127
158
|
#
|
|
128
159
|
# @param message [Carnivore::Message]
|
|
129
160
|
def execute(message)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
#
|
|
142
|
-
# @param params [Hash]
|
|
143
|
-
# @return [Hash] new hash with snake cased toplevel keys
|
|
144
|
-
def transform_parameters(params)
|
|
145
|
-
Smash.new.tap do |new_hash|
|
|
146
|
-
params.each do |key, value|
|
|
147
|
-
new_hash[snakecase(key)] = value
|
|
148
|
-
end
|
|
161
|
+
data_payload = unpack(message)
|
|
162
|
+
payload = new_payload(
|
|
163
|
+
config[:name],
|
|
164
|
+
:cfn_resource => data_payload
|
|
165
|
+
)
|
|
166
|
+
if(config[:reprocess])
|
|
167
|
+
my_input = "#{source_prefix}_input"
|
|
168
|
+
Carnivore::Supervisor.supervisor[my_input].transmit(payload)
|
|
169
|
+
message.confirm!
|
|
170
|
+
else
|
|
171
|
+
completed(payload, message)
|
|
149
172
|
end
|
|
150
173
|
end
|
|
151
174
|
|
|
152
|
-
# Snake case string
|
|
153
|
-
#
|
|
154
|
-
# @param v [String]
|
|
155
|
-
# @return [Symbol]
|
|
156
|
-
def snakecase(v)
|
|
157
|
-
v.to_s.gsub(/(?<![A-Z])([A-Z])/, '_\1').sub(/^_/, '').downcase.to_sym
|
|
158
|
-
end
|
|
159
|
-
|
|
160
175
|
end
|
|
161
176
|
end
|
|
162
177
|
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
require 'jackal-cfn'
|
|
2
|
+
|
|
3
|
+
module Jackal
|
|
4
|
+
module Cfn
|
|
5
|
+
module Utils
|
|
6
|
+
# Helper module for loading Fog APIs
|
|
7
|
+
module Fog
|
|
8
|
+
|
|
9
|
+
include Utils
|
|
10
|
+
|
|
11
|
+
# Provide API for given type
|
|
12
|
+
#
|
|
13
|
+
# @param type [Symbol] Fog API (compute, orchestration, etc)
|
|
14
|
+
# @return [Fog::Service]
|
|
15
|
+
# @note extracts credentials from confg at :api -> [type | :default]
|
|
16
|
+
def api_for(type)
|
|
17
|
+
klass = Fog.constants.detect do |const|
|
|
18
|
+
snakecase(const).to_s == type.to_s
|
|
19
|
+
end
|
|
20
|
+
if(klass)
|
|
21
|
+
credentials = config.fetch(
|
|
22
|
+
:api, type, config.get(
|
|
23
|
+
:api, :default
|
|
24
|
+
)
|
|
25
|
+
)
|
|
26
|
+
if(credentials)
|
|
27
|
+
key = credentials.to_a.flatten.push(klass).sort.hash
|
|
28
|
+
Thread.current[:cfn_apis] ||= Smash.new
|
|
29
|
+
unless(Thread.current[:cfn_apis][key])
|
|
30
|
+
Thread.current[:cfn_apis][key] = Fog.const_get(klass).new(credentials)
|
|
31
|
+
end
|
|
32
|
+
Thread.current[:cfn_apis][key]
|
|
33
|
+
else
|
|
34
|
+
ArgumentError.new 'No credentials provided in configuration!'
|
|
35
|
+
end
|
|
36
|
+
else
|
|
37
|
+
raise TypeError.new "Unknown API type requested (#{type})"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Assume the role for the API connection
|
|
42
|
+
#
|
|
43
|
+
# @param api [Fog::Service]
|
|
44
|
+
# @param role [String] name of role to assume
|
|
45
|
+
# @return [Fog::Service] assumed service
|
|
46
|
+
# @note this is AWS specific
|
|
47
|
+
def api_assume_for(api, role)
|
|
48
|
+
Thread.current[:cfn_assume_apis] ||= Smash.new
|
|
49
|
+
key = api.to_yaml_properties.group_by do |item|
|
|
50
|
+
item.to_s.split('_').first
|
|
51
|
+
end.values.sort_by(&:size).last.map do |var|
|
|
52
|
+
[var, api.instance_variable_get(var)]
|
|
53
|
+
end.flatten.compact.map(&:to_s).push(api.service_name).sort.hash
|
|
54
|
+
if(Thread.current[:cfn_assume_apis].get(key, :expires).to_i < Time.now.to_i + 5)
|
|
55
|
+
sts = Fog::AWS::STS.new(
|
|
56
|
+
config.fetch(
|
|
57
|
+
:api, :sts, config.get(
|
|
58
|
+
:api, :default
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
result = sts.assume_role("jackal-cfn-#{Celluloid.uuid}", role).body
|
|
63
|
+
Thread.current[:cfn_assume_apis][key] = Smash.new(
|
|
64
|
+
:expires => Time.parse(result['Expiration']).to_i,
|
|
65
|
+
:api => api.class.new(
|
|
66
|
+
:aws_access_key_id => result['AccessKeyId'],
|
|
67
|
+
:aws_secret_access_key => result['SecretAccessKey'],
|
|
68
|
+
:aws_session_token => result['SessionToken']
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
Thread.current[:cfn_assume_apis].get(key, :api)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require 'patron'
|
|
2
|
+
require 'jackal-cfn'
|
|
3
|
+
|
|
4
|
+
module Jackal
|
|
5
|
+
module Cfn
|
|
6
|
+
module Utils
|
|
7
|
+
# Helper module for HTTP interactions
|
|
8
|
+
module Http
|
|
9
|
+
|
|
10
|
+
# Provide remote endpoint session for sending response
|
|
11
|
+
#
|
|
12
|
+
# @param host [String] end point host
|
|
13
|
+
# @param scheme [String] end point scheme
|
|
14
|
+
# @return [Patron::Session]
|
|
15
|
+
def response_endpoint(host, scheme)
|
|
16
|
+
session = Patron::Session.new
|
|
17
|
+
session.timeout = config.fetch(:response_timeout, 20)
|
|
18
|
+
session.connect_timeout = config.fetch(:connection_timeout, 10)
|
|
19
|
+
session.base_url = "#{scheme}://#{host}"
|
|
20
|
+
session.headers['User-Agent'] = "JackalCfn/#{Jackal::Cfn::VERSION.version}"
|
|
21
|
+
session
|
|
22
|
+
end
|
|
23
|
+
alias_method :http_endpoint, :response_endpoint
|
|
24
|
+
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require 'jackal-cfn'
|
|
2
|
+
|
|
3
|
+
module Jackal
|
|
4
|
+
module Cfn
|
|
5
|
+
module Utils
|
|
6
|
+
|
|
7
|
+
autoload :Fog, 'jackal-cfn/utils/fog'
|
|
8
|
+
autoload :Http, 'jackal-cfn/utils/http'
|
|
9
|
+
|
|
10
|
+
# Snake case top level keys in hash
|
|
11
|
+
#
|
|
12
|
+
# @param params [Hash]
|
|
13
|
+
# @return [Hash] new hash with snake cased toplevel keys
|
|
14
|
+
def transform_parameters(params)
|
|
15
|
+
Smash.new.tap do |new_hash|
|
|
16
|
+
params.each do |key, value|
|
|
17
|
+
new_hash[snakecase(key)] = value
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
alias_method :rekey_hash, :transform_parameters
|
|
22
|
+
|
|
23
|
+
# Snake case string
|
|
24
|
+
#
|
|
25
|
+
# @param v [String]
|
|
26
|
+
# @return [Symbol]
|
|
27
|
+
def snakecase(v)
|
|
28
|
+
v.to_s.gsub(/(?<![A-Z])([A-Z])/, '_\1').sub(/^_/, '').downcase.to_sym
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/jackal-cfn/version.rb
CHANGED
data/lib/jackal-cfn.rb
CHANGED
|
@@ -6,5 +6,12 @@ module Jackal
|
|
|
6
6
|
module Cfn
|
|
7
7
|
autoload :Event, 'jackal-cfn/event'
|
|
8
8
|
autoload :Resource, 'jackal-cfn/resource'
|
|
9
|
+
autoload :Utils, 'jackal-cfn/utils'
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Tools
|
|
13
|
+
module CfnTools
|
|
14
|
+
autoload :HashExtractor, 'jackal-cfn/resource/hash_extractor'
|
|
15
|
+
autoload :AmiManager, 'jackal-cfn/resource/ami_manager'
|
|
9
16
|
end
|
|
10
17
|
end
|
metadata
CHANGED
|
@@ -1,46 +1,41 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jackal-cfn
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
5
|
-
prerelease:
|
|
4
|
+
version: 0.1.6
|
|
6
5
|
platform: ruby
|
|
7
6
|
authors:
|
|
8
7
|
- Chris Roberts
|
|
9
8
|
autorequire:
|
|
10
9
|
bindir: bin
|
|
11
10
|
cert_chain: []
|
|
12
|
-
date: 2014-
|
|
11
|
+
date: 2014-08-14 00:00:00.000000000 Z
|
|
13
12
|
dependencies:
|
|
14
13
|
- !ruby/object:Gem::Dependency
|
|
15
14
|
name: jackal
|
|
16
15
|
requirement: !ruby/object:Gem::Requirement
|
|
17
|
-
none: false
|
|
18
16
|
requirements:
|
|
19
|
-
- -
|
|
17
|
+
- - ">="
|
|
20
18
|
- !ruby/object:Gem::Version
|
|
21
19
|
version: '0'
|
|
22
20
|
type: :runtime
|
|
23
21
|
prerelease: false
|
|
24
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
-
none: false
|
|
26
23
|
requirements:
|
|
27
|
-
- -
|
|
24
|
+
- - ">="
|
|
28
25
|
- !ruby/object:Gem::Version
|
|
29
26
|
version: '0'
|
|
30
27
|
- !ruby/object:Gem::Dependency
|
|
31
28
|
name: patron
|
|
32
29
|
requirement: !ruby/object:Gem::Requirement
|
|
33
|
-
none: false
|
|
34
30
|
requirements:
|
|
35
|
-
- -
|
|
31
|
+
- - ">="
|
|
36
32
|
- !ruby/object:Gem::Version
|
|
37
33
|
version: '0'
|
|
38
34
|
type: :runtime
|
|
39
35
|
prerelease: false
|
|
40
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
41
|
-
none: false
|
|
42
37
|
requirements:
|
|
43
|
-
- -
|
|
38
|
+
- - ">="
|
|
44
39
|
- !ruby/object:Gem::Version
|
|
45
40
|
version: '0'
|
|
46
41
|
description: CFN callback helpers
|
|
@@ -49,41 +44,43 @@ executables: []
|
|
|
49
44
|
extensions: []
|
|
50
45
|
extra_rdoc_files: []
|
|
51
46
|
files:
|
|
47
|
+
- CHANGELOG.md
|
|
48
|
+
- CONTRIBUTING.md
|
|
49
|
+
- LICENSE
|
|
50
|
+
- README.md
|
|
51
|
+
- jackal-cfn.gemspec
|
|
52
52
|
- lib/jackal-cfn.rb
|
|
53
53
|
- lib/jackal-cfn/event.rb
|
|
54
|
-
- lib/jackal-cfn/version.rb
|
|
55
54
|
- lib/jackal-cfn/resource.rb
|
|
56
|
-
- lib/jackal-cfn/resource/hash_extractor.rb
|
|
57
55
|
- lib/jackal-cfn/resource/ami_manager.rb
|
|
58
|
-
- jackal-cfn.
|
|
59
|
-
-
|
|
60
|
-
-
|
|
61
|
-
-
|
|
62
|
-
-
|
|
56
|
+
- lib/jackal-cfn/resource/hash_extractor.rb
|
|
57
|
+
- lib/jackal-cfn/utils.rb
|
|
58
|
+
- lib/jackal-cfn/utils/fog.rb
|
|
59
|
+
- lib/jackal-cfn/utils/http.rb
|
|
60
|
+
- lib/jackal-cfn/version.rb
|
|
63
61
|
homepage: https://github.com/carnivore-rb/jackal-cfn
|
|
64
62
|
licenses:
|
|
65
63
|
- Apache 2.0
|
|
64
|
+
metadata: {}
|
|
66
65
|
post_install_message:
|
|
67
66
|
rdoc_options: []
|
|
68
67
|
require_paths:
|
|
69
68
|
- lib
|
|
70
69
|
required_ruby_version: !ruby/object:Gem::Requirement
|
|
71
|
-
none: false
|
|
72
70
|
requirements:
|
|
73
|
-
- -
|
|
71
|
+
- - ">="
|
|
74
72
|
- !ruby/object:Gem::Version
|
|
75
73
|
version: '0'
|
|
76
74
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
77
|
-
none: false
|
|
78
75
|
requirements:
|
|
79
|
-
- -
|
|
76
|
+
- - ">="
|
|
80
77
|
- !ruby/object:Gem::Version
|
|
81
78
|
version: '0'
|
|
82
79
|
requirements: []
|
|
83
80
|
rubyforge_project:
|
|
84
|
-
rubygems_version:
|
|
81
|
+
rubygems_version: 2.2.2
|
|
85
82
|
signing_key:
|
|
86
|
-
specification_version:
|
|
83
|
+
specification_version: 4
|
|
87
84
|
summary: Message processing helper
|
|
88
85
|
test_files: []
|
|
89
86
|
has_rdoc:
|