Class: Stacco::Stack

Inherits:
Object
  • Object
show all
Defined in:
lib/stacco/stack.rb

Instance Method Summary collapse

Constructor Details

#initialize(stack_bucket) ⇒ Stack

Returns a new instance of Stack.



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# File 'lib/stacco/stack.rb', line 14

def initialize(stack_bucket)
  @bucket = stack_bucket
  @bucket.cache_dir = Pathname.new(ENV['HOME']) + '.config' + 'stacco' + 'stack' + @bucket.name

  @config_object = @bucket.objects['stack.yml']

  aws_creds = self.aws_credentials
  @services = {
    ec2: AWS::EC2.new(aws_creds),
    s3: AWS::S3.new(aws_creds),
    autoscaling: AWS::AutoScaling.new(aws_creds),
    route53: AWS::Route53.new(aws_creds),
    cloudformation: AWS::CloudFormation.new(aws_creds),
    cloudfront: AWS::CloudFront.new(aws_creds),
    rds: AWS::RDS.new(aws_creds),
    iam: AWS::IAM.new(aws_creds)
  }

  @aws_stack = @services[:cloudformation].stacks[self.name]
  @aws_stack.service_registry = @services
end

Instance Method Details

#available_layer_namesObject



246
247
248
# File 'lib/stacco/stack.rb', line 246

def available_layer_names
  Stacco::Resources::LayerTemplates.keys
end

#available_layersObject



250
251
252
# File 'lib/stacco/stack.rb', line 250

def available_layers
  self.available_layer_names.map{ |layer_name| Stacco::Layer.load(self, layer_name) }
end

#aws_credentialsObject



144
145
146
# File 'lib/stacco/stack.rb', line 144

def aws_credentials
  Hash[ *(self.config['aws'].map{ |k, v| [k.intern, v] }.flatten) ]
end

#aws_statusObject



75
76
77
# File 'lib/stacco/stack.rb', line 75

def aws_status
  @aws_stack.status
end

#bake_template(opts = {}) ⇒ Object



166
167
168
169
170
171
172
173
174
175
176
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
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
# File 'lib/stacco/stack.rb', line 166

def bake_template(opts = {})
  publish_first = opts.delete(:publish)

  baked_template_body = self.cloudformation_template_body

  env_lns = [
    "cat >/etc/environment.local <<EOF",
    self.config.find_all{ |k, v| v.kind_of?(String) }.map{ |(k, v)| "export #{k.to_s.upcase}=\"#{v}\"" },
    self.secrets.find_all{ |k, v| v.kind_of?(String) }.map{ |k, v| "export #{k.to_s.upcase}=\"#{v}\"" },
    "EOF",
    "source /etc/environment.local"
  ].flatten.map{ |ln| ln + "\n" }

  parameters = {
    'IAMKeypairNameVar' => self.iam_keypair_name,
    'MainDBAdminUsernameVar' => self.secrets['db_admin_username'],
    'MainDBAdminPasswordVar' => self.secrets['db_admin_password'],
    'DBSnapshotVar' => (self.config['db_snapshot'] || ""),
    'EnvironmentTypeVar' => self.config['environment'],
    'UserDataEnvironmentVar' => env_lns.join
  }

  (self.config['permit_backoffice_access'] || []).each do |rule_name, (auth_type, auth_opts)|
    case auth_type
    when :ip_range
      parameters["#{rule_name.capitalize}IPRange"] = auth_opts
    end
  end

  scaling_groups = self.config['scale']
  self.enabled_layer_names.each do |layer_name|
    next unless scaling_groups.has_key?(layer_name)
    camelized_layer_name = layer_name.split('-').map{ |w| w.capitalize.gsub(/api/i, 'API') }.join
    parameters["Min#{camelized_layer_name}Var"] = scaling_groups[layer_name].to_s
    parameters["Max#{camelized_layer_name}Var"] = (scaling_groups[layer_name] + 1).to_s
  end

  if instance_ami = self.config['base_image']
    parameters['InstanceAMIVar'] = instance_ami
  end

  Stacco::Resources::RoleScripts.each do |role_name, role_script|
    parameters["#{role_name}RoleScriptVar"] = role_script
  end


  bake_id = '%d-%04x' % [Time.now.to_i, rand(36 ** 4)]
  template_object = @bucket.objects["template/#{bake_id}"]

  if publish_first
    template_object.write(baked_template_body, acl: :authenticated_read)
  end

  return [template_object, parameters] unless block_given?

  if block_given?
    new_template_body = yield(baked_template_body, parameters)
  else
    new_template_body = baked_template_body
  end

  unless publish_first and new_template_body == baked_template_body
    if new_template_body
      template_object.write(new_template_body, acl: :authenticated_read)
    else
      template_object.delete if template_object.exists?
    end
  end

  [template_object, parameters]
end

#cancel_operationObject



65
66
67
68
# File 'lib/stacco/stack.rb', line 65

def cancel_operation
  return unless self.operation_in_progress?
  @aws_stack.cancel_update
end

#cancel_operation!Object



70
71
72
73
# File 'lib/stacco/stack.rb', line 70

def cancel_operation!
  self.cancel_operation
  Kernel.sleep(2) while self.operation_in_progress?
end

#cloudformation_templateObject



303
304
305
# File 'lib/stacco/stack.rb', line 303

def cloudformation_template
  Stacco::Template.const_get(self.config['template']).new
end

#cloudformation_template_bodyObject



307
308
309
# File 'lib/stacco/stack.rb', line 307

def cloudformation_template_body
  self.cloudformation_template.to_json(stack: self)
end

#configObject



99
100
101
# File 'lib/stacco/stack.rb', line 99

def config
  YAML.load(@config_object.read)
end

#config=(new_config) ⇒ Object



103
104
105
# File 'lib/stacco/stack.rb', line 103

def config=(new_config)
  @config_object.write(new_config.to_yaml)
end

#connectionsObject



36
37
38
39
40
41
# File 'lib/stacco/stack.rb', line 36

def connections
  connections = {}
  running_instances = @aws_stack.instances.find_all{ |i| i.status == :running }
  running_instances.each{ |i| connections[i.tags["aws:cloudformation:logical-id"]] = i }
  connections
end

#databasesObject



43
44
45
46
47
48
# File 'lib/stacco/stack.rb', line 43

def databases
  @aws_stack.rds_instances.inject({}) do |dbs, (k, v)|
    (dbs[k] = v) if v.status == "available"
    dbs
  end
end

#descriptionObject



148
149
150
# File 'lib/stacco/stack.rb', line 148

def description
  self.config['description']
end

#disable_layers(layer_names) ⇒ Object



124
125
126
127
128
129
130
# File 'lib/stacco/stack.rb', line 124

def disable_layers(layer_names)
  layer_names = layer_names.map(&:to_s)

  self.update_config do |c|
    c['layers'] = self.enabled_layer_names - layer_names
  end
end

#domainObject



83
84
85
86
87
# File 'lib/stacco/stack.rb', line 83

def domain
  domain = Stacco::Domain.new(self, self.config['domain'].gsub(/\.$/, '').split('.').reverse)
  domain.service_registry = @services
  domain
end

#down!Object



294
295
296
297
298
299
300
301
# File 'lib/stacco/stack.rb', line 294

def down!
  return false unless self.up?

  @aws_stack.buckets.each{ |bucket| bucket.delete! }
  @aws_stack.delete

  true
end

#enable_layers(layer_names) ⇒ Object



113
114
115
116
117
118
119
120
121
122
# File 'lib/stacco/stack.rb', line 113

def enable_layers(layer_names)
  layer_names = layer_names.map(&:to_s)
  layer_names.each do |layer_name|
    raise ArgumentError, "Layer '#{layer_name}' is not provided by the template definition" unless self.available_layer_names.include? layer_name
  end

  self.update_config do |c|
    c['layers'] = self.enabled_layer_names | layer_names
  end
end

#enabled_layer_namesObject



132
133
134
# File 'lib/stacco/stack.rb', line 132

def enabled_layer_names
  (self.available_layer_names & (self.config['layers'] || []))
end

#enabled_layersObject



136
137
138
# File 'lib/stacco/stack.rb', line 136

def enabled_layers
  self.enabled_layer_names.map{ |layer_name| Stacco::Layer.load(self, layer_name) }
end

#iam_keypair_nameObject



337
338
339
# File 'lib/stacco/stack.rb', line 337

def iam_keypair_name
  "stacco-%s-%s" % [self.name, self.iam_private_key.key.split('/').last]
end

#iam_private_keyObject



333
334
335
# File 'lib/stacco/stack.rb', line 333

def iam_private_key
  @bucket.objects.with_prefix("ssh-key/").to_a.sort_by{ |obj| obj.key.split('/').last.to_i }.last
end

#initialize_distributions!Object



279
280
281
282
283
284
285
286
287
288
# File 'lib/stacco/stack.rb', line 279

def initialize_distributions!
  @services[:cloudfront].distributions.each do |dist|
    dist.update do
      next unless stack_dist_cert = @aws_stack.server_certificates(domain: dist.aliases).first

      dist.price_class = :"100"
      dist.certificate = stack_dist_cert.id
    end
  end
end

#invalidate_distributed_objects!(dist_cname, obj_keys) ⇒ Object



290
291
292
# File 'lib/stacco/stack.rb', line 290

def invalidate_distributed_objects!(dist_cname, obj_keys)
  @aws_stack.distribution(dist_cname).invalidate(obj_keys)
end

#layer_enabled?(layer_name) ⇒ Boolean

Returns:

  • (Boolean)


140
141
142
# File 'lib/stacco/stack.rb', line 140

def layer_enabled?(layer_name)
  self.enabled_layer_names.inlude?(layer_name.to_s)
end

#must_be_up!Object



54
55
56
57
58
59
# File 'lib/stacco/stack.rb', line 54

def must_be_up!
  unless self.up?
    $stderr.puts "stack #{self.name} is down"
    Kernel.exit 1
  end
end

#nameObject



152
153
154
# File 'lib/stacco/stack.rb', line 152

def name
  self.config['name']
end

#name=(new_name) ⇒ Object



156
157
158
159
160
# File 'lib/stacco/stack.rb', line 156

def name=(new_name)
  update_config do |c|
    c['name'] = new_name
  end
end

#operation_in_progress?Boolean

Returns:

  • (Boolean)


61
62
63
# File 'lib/stacco/stack.rb', line 61

def operation_in_progress?
  @aws_stack.exists? and @aws_stack.status =~ /_IN_PROGRESS$/
end

#resource_summariesObject



50
51
52
# File 'lib/stacco/stack.rb', line 50

def resource_summaries
  @aws_stack.resource_summaries
end

#rolesObject



242
243
244
# File 'lib/stacco/stack.rb', line 242

def roles
  Stacco::Resources::RoleScripts.keys
end

#secretsObject



238
239
240
# File 'lib/stacco/stack.rb', line 238

def secrets
  self.config['secrets']
end

#statusObject



79
80
81
# File 'lib/stacco/stack.rb', line 79

def status
  self.up? ? self.aws_status : "DOWN"
end

#stream_eventsObject



341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
# File 'lib/stacco/stack.rb', line 341

def stream_events
  Enumerator.new do |out|
    known_events = Set.new
    ticks_without_add = 0
    current_tick = 0
    current_op = nil
    tracked_resources = Set.new

    while true
      added = 0

      stack_events = @aws_stack.events.to_a rescue []

      current_resources = [@aws_stack.instances, @aws_stack.distributions].flatten
      current_resources.each do |new_rs|
        next if tracked_resources.member? new_rs
        tracked_resources.add new_rs
        new_rs.instance_variable_set('@prev_status', :nonexistent)
      end

      tracked_resources.each do |rs|
        resource_name = [rs.tags['aws:cloudformation:logical-id']]
        if rs.tags['aws:autoscaling:groupName']
          resource_name.push(rs.id.split('-')[1])
        end
        resource_name = resource_name.compact.join('.')

        resource_is_live = (current_tick > 0)
        resource_status_delta = rs.change_in_status

        if resource_is_live and resource_status_delta
          now = Time.now
          evt = OpenStruct.new(
            event_id: "#{rs.id}#{now.to_i}#{resource_status_delta.inspect}",
            live: true,
            logical_resource_id: resource_name,
            status: "CHANGED",
            operation: "UPDATE",
            timestamp: now,
            error: "#{resource_status_delta[:from]} -> #{resource_status_delta[:to]}",
            detail: nil
          )

          if resource_status_delta[:to] == :terminated and rs.respond_to?(:console_output) and logs = rs.console_output
            logs = logs.split("\r\n")
            if cfn_signal_ln = logs.grep("CloudFormation signaled successfully with FAILURE.").last
              logs = logs[0 ... logs.index(cfn_signal_ln)]
            end
            logs = logs[-30 .. -1]
            evt.detail = logs.map{ |ln| ln }
          end

          stack_events.push evt
        end
      end

      stack_events = stack_events.sort_by{ |ev| ev.timestamp }

      stack_events.each do |event|
        next if known_events.include? event.event_id
        known_events.add event.event_id

        if event.resource_type == "AWS::CloudFormation::Stack"
          current_op = event
        end

        event.live = (current_tick > 0)
        event.op = current_op

        out.yield event

        added += 1
        ticks_without_add = 0
      end

      if current_tick == 0 and stack_events.last.op
        stack_events.last.op.live = true
        stack_events.each{ |ev| out.yield(ev) if (ev.op and ev.op.live) }
      end

      current_tick += 1
      ticks_without_add += 1 if added == 0

      if ticks_without_add >= 8 and (Math.log2(ticks_without_add) % 1) == 0.0
        jobs = @aws_stack.resource_summaries
        active_jobs = jobs.find_all{ |job| job[:resource_status] =~ /IN_PROGRESS$/ }.map{ |job| job[:logical_resource_id] }.sort
        unless active_jobs.empty?
          out.yield OpenStruct.new(
            live: true,
            logical_resource_id: "Scheduler",
            status: "WAIT",
            operation: "WAIT",
            timestamp: Time.now,
            error: "waiting on #{active_jobs.join(', ')}",
            detail: nil
          )
        end
      end

      Kernel.sleep 2
    end
  end
end

#subdomainsObject



89
90
91
92
93
94
95
96
97
# File 'lib/stacco/stack.rb', line 89

def subdomains
  d = self.domain

  self.config['subdomains'].map do |logical_name, prefix_parts|
    sd = prefix_parts.inject(d, &:+)
    sd.logical_name = logical_name
    sd
  end
end

#up!Object



254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
# File 'lib/stacco/stack.rb', line 254

def up!
  body_object, params = self.bake_template(publish: true)

  unless @aws_stack.exists?
    return @services[:cloudformation].stacks.create(
      self.name,
      body_object.public_url,
      parameters: params
    )
    #disable_rollback: true
  end

  begin
    @aws_stack.update(template: body_object.public_url, parameters: params)
    true
  rescue AWS::CloudFormation::Errors::ValidationError => e
    raise unless e.message =~ /no updates/i
    false
  end
end

#up?Boolean

Returns:

  • (Boolean)


162
163
164
# File 'lib/stacco/stack.rb', line 162

def up?
  @aws_stack.exists?
end

#up_sinceObject



275
276
277
# File 'lib/stacco/stack.rb', line 275

def up_since
  @aws_stack.creation_time if @aws_stack.exists?
end

#update_config {|new_config| ... } ⇒ Object

Yields:

  • (new_config)


107
108
109
110
111
# File 'lib/stacco/stack.rb', line 107

def update_config
  new_config = self.config
  yield(new_config)
  self.config = new_config
end

#validateObject



311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
# File 'lib/stacco/stack.rb', line 311

def validate
  body_object, _ = self.bake_template do |body, params|
    tpl.gsub(/"[cm][123]\.(\dx)?(small|medium|large)"/, '"m1.small"')
  end

  Kernel.sleep 1

  begin
    @services[:cloudformation].estimate_template_cost(body_object)
    [true]
  rescue AWS::CloudFormation::Errors::ValidationError => e
    msg = e.message
    match = msg.scan(/^Template format error: JSON not well-formed. \(line (\d+), column (\d+)\)$/)
    if match.length.nonzero?
      line, column = match.to_a.flatten.map{ |el| el.to_i }
      [false, msg, [baked_template_object.read.split("\n")[line.to_i], column]]
    else
      [false, msg]
    end
  end
end