Class: Jackal::Cfn::JackalStack

Inherits:
Resource
  • Object
show all
Defined in:
lib/jackal-cfn/resource/jackal_stack.rb

Overview

Manage AMI Resources

Expected resource:

{
  "Type": "Custom::JackalStack",
  "Properties": {
    "Parameters": {
      STACK_PARAMETERS
    },
    "Location": LOCATION,
    "TemplateURL": "URL"
  }
}

Required configuration:

{
  "config": {
    "jackal_stack": {
      "credentials": {
        "storage": {
          AWS_CREDENTIALS
        },
        LOCATION: {
          "provider": "NAME",
          MIAMSA_CREDENTIALS
        }
      }
    }
  }
}

Constant Summary collapse

LOCATION_JOINER =
'__~__'

Constants inherited from Resource

Resource::VALID_RESOURCE_STATUS

Instance Method Summary collapse

Methods inherited from Resource

#build_response, #failure_wrap, inherited, #physical_resource_id, #respond_to_stack, #unpack, #valid?

Methods included from Utils::Http

#response_endpoint

Methods included from Utils

#snakecase, #transform_parameters

Instance Method Details

#create_stack(response, resource, properties, parameters, message) ⇒ TrueClass, FalseClass

Create a new stack and update the response values

Parameters:

  • response (Hash)

    response data of action

  • resource (Hash)

    request resource

  • properties (Hash)

    properties of request resource

  • parameters (Hash)

    parmeters provided via properties

  • message (Carnivore::Message)

    original message

Returns:

  • (TrueClass, FalseClass)


139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/jackal-cfn/resource/jackal_stack.rb', line 139

def create_stack(response, resource, properties, parameters, message)
  stack = remote_api(properties[:location]).stacks.build(
    :name => generate_stack_name(resource),
    :template => properties.fetch(:stack, fetch_template(properties[:template_url])),
    :parameters => Hash[parameters.map{|k,v| [Bogo::Utility.camel(k), v] }]
  )
  stack.save
  until(stack.state.to_s.end_with?('complete'))
    message.touch!
    debug "Waiting for created stack to reach completion..."
    sleep 5
    stack.reload
  end
  if(stack.state.to_s.end_with?('complete') || stack.state.to_s.end_with?('failed'))
    stack.outputs.each do |output|
      response['Data']["Outputs.#{output.key}"] = output.value
    end
    response['PhysicalResourceId'] = [
      properties[:location],
      stack.id
    ].join(LOCATION_JOINER)
    true
  else
    response['Status'] = 'FAILED'
    response['Reason'] = 'Stack creation failed!'
    stack.destroy
    false
  end
end

#destroy_stack(response, resource, message) ⇒ Object

Destroy the stack

Parameters:

  • response (Hash)

    response data of action

  • resource (Hash)

    request resource

  • message (Carnivore::Message)

    original message



221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/jackal-cfn/resource/jackal_stack.rb', line 221

def destroy_stack(response, resource, message)
  stack = request_destroy(resource[:physical_resource_id])
  unless(stack)
    properties = rekey_hash(resource[:resource_properties])
    stack = request_destroy(
      [
        properties[:location],
        generate_stack_name(resource)
      ].join(LOCATION_JOINER)
    )
  end
  if(stack)
    until(stack.state.nil? || stack.state.to_s.end_with?('complete') || stack.state.to_s.end_with?('failed'))
      info "Waiting for stack destruction (#{stack.name})..."
      message.touch!
      sleep 5
      stack.reload
    end
    if(stack.state.to_s.end_with?('failed'))
      response['Status'] = 'FAILED'
      response['Reason'] = 'Failed to delete remote stack!'
    end
  end
end

#execute(message) ⇒ Object

Perform requested stack action

Parameters:

  • message (Carnivore::Message)


47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/jackal-cfn/resource/jackal_stack.rb', line 47

def execute(message)
  failure_wrap(message) do |payload|
    cfn_resource = rekey_hash(payload.get(:data, :cfn_resource))
    properties = rekey_hash(cfn_resource[:resource_properties])
    parameters = rekey_hash(properties[:parameters])
    cfn_response = build_response(cfn_resource)
    case cfn_resource[:request_type].to_sym
    when :create
      create_stack(cfn_response, cfn_resource, properties, parameters, message)
    when :update
      update_stack(cfn_response, cfn_resource, properties, parameters, message)
    when :delete
      destroy_stack(cfn_response, cfn_resource, message)
    else
      error "Unknown request type received: #{cfn_resource[:request_type].inspect}"
      cfn_response['Status'] = 'FAILED'
      cfn_response['Reason'] = 'Unknown request type received'
    end
    respond_to_stack(cfn_response, cfn_resource[:response_url])
    job_completed(:jackal_cfn, payload, message)
  end
end

#fetch_template(endpoint) ⇒ Hash

Fetch a template from a storage bucket

Parameters:

  • endpoint (String)

    URL to template

Returns:

  • (Hash)

    loaded template data



105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/jackal-cfn/resource/jackal_stack.rb', line 105

def fetch_template(endpoint)
  url = URI.parse(endpoint)
  region = url.host.split('.').first.split('-', 2).last
  if(region == 's3')
    region = 'us-east-1'
  end
  bucket, path = url.path.sub('/', '').split('/', 2)
  MultiJson.load(
    storage_api(region).buckets.get(
      bucket.sub('/', '')
    ).files.get(path).body.read
  )
end

#generate_stack_name(resource) ⇒ String

Generate the remote stack name via the resource information

Parameters:

  • resource (Hash)

Returns:

  • (String)


123
124
125
126
127
128
129
# File 'lib/jackal-cfn/resource/jackal_stack.rb', line 123

def generate_stack_name(resource)
  [
    'JackalStack',
    resource[:logical_resource_id],
    resource[:stack_id].split('/').last
  ].join('-')
end

#remote_api(location) ⇒ Miasma::Models::Orchestration

Build orchestration API connection for provided location

Parameters:

  • location (String, Symbol)

Returns:

  • (Miasma::Models::Orchestration)


88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/jackal-cfn/resource/jackal_stack.rb', line 88

def remote_api(location)
  l_config = config.get(:jackal_stack, :credentials, location)
  if(l_config)
    Miasma.api(
      :type => :orchestration,
      :provider => l_config[:provider],
      :credentials => l_config
    )
  else
    raise ArgumentError.new "Unknown target location provided `#{location}`!"
  end
end

#request_destroy(stack_resource_id) ⇒ Miasma::Models::Orchestration::Stack, FalseClass

Send a stack delete request

Parameters:

  • stack_resource_id (String)

    physical resource ID

Returns:

  • (Miasma::Models::Orchestration::Stack, FalseClass)


250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
# File 'lib/jackal-cfn/resource/jackal_stack.rb', line 250

def request_destroy(stack_resource_id)
  location, stack_id = stack_resource_id.split(LOCATION_JOINER, 2)
  if(stack_id)
    begin
      info "Sending stack destruction request to: #{stack_id} in: #{location}"
      stack = remote_api(location).stacks.get(stack_id)
      stack.destroy
      stack
    rescue => e
      error "Stack destruction request failed! #{e.class}: #{e.message}"
      false
    end
  else
    warn "No stack ID registered in resource. Skipping destroy: #{stack_resource_id}"
    false
  end
end

#setup(*_) ⇒ Object

Load miasma for stack building



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

def setup(*_)
  require 'miasma'
end

#storage_api(bucket_region) ⇒ Miasma::Models::Storage

Build API connection to base template storage bucket

Parameters:

  • bucket_region (String)

    location of bucket

Returns:

  • (Miasma::Models::Storage)


74
75
76
77
78
79
80
81
82
# File 'lib/jackal-cfn/resource/jackal_stack.rb', line 74

def storage_api(bucket_region)
  Miasma.api(
    :type => :storage,
    :provider => :aws,
    :credentials => config.get(:jackal_stack, :credentials, :storage).merge(
      :aws_bucket_region => bucket_region
    )
  )
end

#update_stack(response, resource, properties, parameters, message) ⇒ TrueClass, FalseClass

Update an existing stack and update the response values

Parameters:

  • response (Hash)

    response data of action

  • resource (Hash)

    request resource

  • properties (Hash)

    properties of request resource

  • parameters (Hash)

    parmeters provided via properties

  • message (Carnivore::Message)

    original message

Returns:

  • (TrueClass, FalseClass)


177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/jackal-cfn/resource/jackal_stack.rb', line 177

def update_stack(response, resource, properties, parameters, message)
  c_location, stack_id = resource[:physical_resource_id].split('-', 2)
  if(c_location != properties[:location])
    warn "Stack resource has changed location! #{c_location} -> #{properties[:location]}"
    warn "Starting destruction of existing resource: #{stack_id}"
    if(destroy_stack(response, resource, message))
      info "Destruction of stack `#{stack_id}` complete. Creating replacement stack."
      create_stack(response, resource, properties, parameters, message)
    else
      error "Failed to destroy existing stack for replacement `#{stack_id}`"
    end
  else
    stack = remote_api(c_location).stacks.get(stack_id)
    if(stack)
      info "Stack resource update on: #{stack_id}"
      stack.template = fetch_template(properties['TemplateURL'])
      stack.parameters = Hash[parameters.map{|k,v| [Bogo::Utility.camel(k), v] }]
      stack.save
      until(stack.state.to_s.end_with?('complete') || stack.state.to_s.end_with?('failed'))
        debug "Waiting for created stack to reach completion..."
        sleep 5
        stack.reload
      end
      if(stack.state.to_s.end_with?('complete'))
        stack.outputs.each do |output|
          response['Data']["Outputs.#{output.key}"] = output.value
        end
        response['PhysicalResourceId'] = stack.id
      else
        response['Status'] = 'FAILED'
        response['Reason'] = 'Stack update failed!'
      end
    else
      response['Status'] = 'FAILED'
      response['Reason'] = "No stack was found matching request: #{stack_id}"
    end
  end
end