Class: Jackal::Cfn::Resource

Inherits:
Jackal::Callback
  • Object
show all
Includes:
Utils, Utils::Http
Defined in:
lib/jackal-cfn/resource.rb

Overview

Callback for resource types

Defined Under Namespace

Modules: InheritedValidity

Constant Summary collapse

VALID_RESOURCE_STATUS =
['SUCCESS', 'FAILED']

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Utils::Http

#response_endpoint

Methods included from Utils

#snakecase, #transform_parameters

Class Method Details

.inherited(klass) ⇒ Object

Update validity checks in subclasses

Parameters:

  • klass (Class)


40
41
42
43
44
# File 'lib/jackal-cfn/resource.rb', line 40

def self.inherited(klass)
  klass.class_eval do
    include InheritedValidity
  end
end

Instance Method Details

#build_response(cfn_resource) ⇒ Hash

Generate response hash

Parameters:

  • cfn_resource (Hash)

Options Hash (cfn_resource):

  • :logical_resource_id (String)
  • :physical_resource_id (String)
  • :stack_id (String)
  • :request_id (String)

Returns:

  • (Hash)

    default response content



83
84
85
86
87
88
89
90
91
92
# File 'lib/jackal-cfn/resource.rb', line 83

def build_response(cfn_resource)
  Smash.new(
    'LogicalResourceId' => cfn_resource[:logical_resource_id],
    'PhysicalResourceId' => cfn_resource.fetch(:physical_resource_id, physical_resource_id),
    'StackId' => cfn_resource[:stack_id],
    'RequestId' => cfn_resource[:request_id],
    'Status' => 'SUCCESS',
    'Data' => Smash.new
  )
end

#execute(message) ⇒ Object

Generate payload and drop

Parameters:

  • message (Carnivore::Message)


156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/jackal-cfn/resource.rb', line 156

def execute(message)
  data_payload = unpack(message)
  payload = new_payload(
    config.fetch(:name, :jackal_cfn),
    :cfn_resource => data_payload
  )
  if(config[:reprocess])
    debug "Reprocessing received message! #{payload}"
    Carnivore::Supervisor.supervisor[destination(:input, payload)].transmit(payload)
    message.confirm!
  else
    completed(payload, message)
  end
end

#failure_wrap(message) ⇒ Object

Custom wrap to send resource failure

Parameters:

  • message (Carnivore::Message)


174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/jackal-cfn/resource.rb', line 174

def failure_wrap(message)
  begin
    payload = unpack(message)
    yield payload
  rescue => e
    error "Unexpected error encountered processing custom resource - #{e.class}: #{e.message}"
    debug "#{e.class}: #{e}\n#{e.backtrace.join("\n")}"
    cfn_resource = payload.get(:data, :cfn_resource)
    cfn_response = build_response(cfn_resource)
    cfn_response['Status'] = 'FAILED'
    cfn_response['Reason'] = "Unexpected error encountered [#{e.message}]"
    respond_to_stack(cfn_response, cfn_resource[:response_url])
    message.confirm!
  end
end

#physical_resource_idString

Note:

this should be overridden in subclasses when actual resources are being created

Physical ID of the resource created

Returns:

  • (String)


71
72
73
# File 'lib/jackal-cfn/resource.rb', line 71

def physical_resource_id
  "#{self.class.name.split('::').last}-#{Carnivore.uuid}"
end

#respond_to_stack(response, response_url) ⇒ TrueClass, FalseClass

Send response to the waiting stack

Parameters:

  • response (Hash)
  • response_url (String)

    response endpoint

Returns:

  • (TrueClass, FalseClass)


99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/jackal-cfn/resource.rb', line 99

def respond_to_stack(response, response_url)
  unless(VALID_RESOURCE_STATUS.include?(response['Status']))
    raise ArgumentError.new "Invalid resource status provided. Got: #{response['Status']}. Allowed: #{VALID_RESOURCE_STATUS.join(', ')}"
  end
  if(response['Status'] == 'FAILED' && !response['Reason'])
    response['Reason'] = 'Unknown'
  end
  url = URI.parse(response_url)
  connection = response_endpoint(url.host, url.scheme)
  path = "#{url.path}?#{url.query}"
  debug "Custom resource response data: #{response.inspect}"
  complete = connection.put(path, JSON.dump(response))
  case complete.status
  when 200
    info "Custom resource response complete! (Sent to: #{url})"
    true
  when 403
    error "Custom resource response failed. Endpoint is forbidden (403): #{url}"
    false
  when 404
    error "Custom resource response failed. Endpoint is not found (404): #{url}"
    false
  else
    raise "Response failed. Received status: #{complete.status} endpoint: #{url}"
  end
end

#setup(*_) ⇒ Object

Setup the dependency requirements for the callback



62
63
64
# File 'lib/jackal-cfn/resource.rb', line 62

def setup(*_)
  require 'patron'
end

#unpack(message) ⇒ Smash

Unpack message and create payload

Parameters:

  • message (Carnivore::Message)

Returns:

  • (Smash)


130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/jackal-cfn/resource.rb', line 130

def unpack(message)
  payload = super
  if(self.is_a?(Jackal::Cfn::Resource))
    begin
      if(payload['Message'])
        payload = MultiJson.load(payload['Message']).to_smash
        payload = transform_parameters(payload)
        payload[:origin_type] = message[:message].get('Body', 'Type')
        payload[:origin_subject] = message[:message].get('Body', 'Subject')
        payload[:request_type] = snakecase(payload[:request_type])
        payload
      else
        payload.to_smash.fetch('Attributes', 'Body', payload.to_smash.fetch('Body', payload.to_smash))
      end
    rescue MultiJson::ParseError
      # Not our expected format so return empty payload
      Smash.new
    end
  else
    payload.to_smash.fetch('Attributes', 'Body', payload.to_smash.fetch('Body', payload.to_smash))
  end
end

#valid?(message) ⇒ TrueClass, FalseClass

Determine message validity

Parameters:

  • message (Carnivore::Message)

Returns:

  • (TrueClass, FalseClass)


50
51
52
53
54
55
56
57
58
59
# File 'lib/jackal-cfn/resource.rb', line 50

def valid?(message)
  super do |payload|
    if(block_given?)
      yield payload
    else
      payload[:origin_type] == 'Notification' &&
        payload[:origin_subject].to_s.downcase.include?('cloudformation custom resource')
    end
  end
end