./lib/stacco/stack.rb in stacco-0.1.18 vs ./lib/stacco/stack.rb in stacco-0.1.20
- old
+ new
@@ -1,9 +1,10 @@
require 'set'
require 'ostruct'
require 'yaml'
require 'pathname'
+require 'base64'
require 'aws/with-stacco-patches'
require 'stacco/base'
@@ -18,10 +19,12 @@
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),
iam: AWS::IAM.new(aws_creds)
}
@@ -44,30 +47,89 @@
$stderr.puts "stack #{self.name} is down"
Kernel.exit 1
end
end
+ def operation_in_progress?
+ @aws_stack.exists? and @aws_stack.status =~ /_IN_PROGRESS$/
+ end
+
+ def cancel_operation
+ return unless self.operation_in_progress?
+ @aws_stack.cancel_update
+ end
+
+ def cancel_operation!
+ self.cancel_operation
+ Kernel.sleep(2) while self.operation_in_progress?
+ end
+
def aws_status
@aws_stack.status
end
def status
self.up? ? self.aws_status : "DOWN"
end
+ def domain
+ domain = Stacco::Domain.new(self, self.config['domain'].gsub(/\.$/, '').split('.').reverse)
+ domain.service_registry = @services
+ domain
+ end
+
+ 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
+
def config
YAML.load(@config_object.read)
end
def config=(new_config)
@config_object.write(new_config.to_yaml)
end
def update_config
- # TODO
+ new_config = self.config
+ yield(new_config)
+ self.config = new_config
end
+ 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_layers.include? layer_name
+ end
+
+ self.update_config do |c|
+ c['layers'] = self.enabled_layers | layer_names
+ end
+ end
+
+ def disable_layers(layer_names)
+ layer_names = layer_names.map(&:to_s)
+
+ self.update_config do |c|
+ c['layers'] = self.enabled_layers - layer_names
+ end
+ end
+
+ def enabled_layers
+ self.available_layers & (self.config['layers'] || [])
+ end
+
+ def layer_enabled?(layer_name)
+ self.enabled_layers.inlude?(layer_name.to_s)
+ end
+
def aws_credentials
Hash[ *(self.config['aws'].map{ |k, v| [k.intern, v] }.flatten) ]
end
def description
@@ -77,25 +139,102 @@
def name
self.config['name']
end
def name=(new_name)
- update_config{ |config| config.merge("name" => new_name) }
+ update_config do |c|
+ c['name'] = new_name
+ end
end
def up?
@aws_stack.exists?
end
- def up!
- if @aws_stack.exists?
- @aws_stack.update(template: self.cloudformation_template)
+ 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.map{ |k, v| "export #{k.to_s.upcase}=\"#{v}\"" },
+ "EOF",
+ "source /etc/environment.local"
+ ].flatten.map{ |ln| ln + "\n" }
+
+ parameters = {
+ 'DockerLibrarySnapshotVar' => self.config['docker_library_snapshot'],
+ 'IAMKeypairNameVar' => self.iam_keypair_name,
+ 'UserDataEnvironmentVar' => env_lns.join
+ }
+
+ 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
- @services[:cloudformation].stacks.create(self.name, self.cloudformation_template)
+ 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
+ def secrets
+ self.config['secrets']
+ end
+
+ def roles
+ Stacco::Resources::RoleScripts.keys
+ end
+
+ def available_layers
+ Stacco::Resources::LayerTemplates.keys
+ end
+
+ 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
+
def up_since
@aws_stack.creation_time if @aws_stack.exists?
end
def initialize_distributions!
@@ -115,75 +254,141 @@
true
end
def cloudformation_template
- #Kernel.eval(Stacco::Resources::Templates[:cloudformation])
- tpl = Stacco::Template.const_get(self.config['template']).new
- tpl.to_json(stack: self)
+ Stacco::Template.const_get(self.config['template']).new
end
def cloudformation_template_body
- Stacco::Resources::Templates[:cloudformation]
+ self.cloudformation_template.to_json(stack: self)
end
def validate
- baked_template = self.cloudformation_template
- test_template = baked_template.gsub(/"[cm][123]\.(\dx)?(small|medium|large)"/, '"m1.small"')
+ 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 test_template
+ @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.split("\n")[line.to_i], column]]
+ [false, msg, [baked_template_object.read.split("\n")[line.to_i], column]]
else
[false, msg]
end
end
end
def iam_private_key
- @config_object.bucket.objects.with_prefix("sshkey/#{self.name}-").to_a.sort_by{ |obj| obj.key.split('/').last.split('-').last.to_i }.last
+ @bucket.objects.with_prefix("ssh-key/").to_a.sort_by{ |obj| obj.key.split('/').last.to_i }.last
end
def iam_keypair_name
- "stacco-" + self.iam_private_key.key.split('/').last
+ "stacco-%s-%s" % [self.name, self.iam_private_key.key.split('/').last]
end
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 self.up?
+ while true
added = 0
- @aws_stack.events.sort_by{ |ev| ev.timestamp }.each do |event|
+ 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
- known_events.add event.event_id
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(', ')}"
+ error: "waiting on #{active_jobs.join(', ')}",
+ detail: nil
)
end
end
Kernel.sleep 2