Class: Stacco::Stack
- Inherits:
-
Object
- Object
- Stacco::Stack
- Defined in:
- lib/stacco/stack.rb
Instance Method Summary collapse
- #available_layer_names ⇒ Object
- #available_layers ⇒ Object
- #aws_credentials ⇒ Object
- #aws_status ⇒ Object
- #bake_template(opts = {}) ⇒ Object
- #cancel_operation ⇒ Object
- #cancel_operation! ⇒ Object
- #cloudformation_template ⇒ Object
- #cloudformation_template_body ⇒ Object
- #config ⇒ Object
- #config=(new_config) ⇒ Object
- #connections ⇒ Object
- #databases ⇒ Object
- #description ⇒ Object
- #disable_layers(layer_names) ⇒ Object
- #domain ⇒ Object
- #down! ⇒ Object
- #enable_layers(layer_names) ⇒ Object
- #enabled_layer_names ⇒ Object
- #enabled_layers ⇒ Object
- #iam_keypair_name ⇒ Object
- #iam_private_key ⇒ Object
-
#initialize(stack_bucket) ⇒ Stack
constructor
A new instance of Stack.
- #initialize_distributions! ⇒ Object
- #invalidate_distributed_objects!(dist_cname, obj_keys) ⇒ Object
- #layer_enabled?(layer_name) ⇒ Boolean
- #must_be_up! ⇒ Object
- #name ⇒ Object
- #name=(new_name) ⇒ Object
- #operation_in_progress? ⇒ Boolean
- #resource_summaries ⇒ Object
- #roles ⇒ Object
- #secrets ⇒ Object
- #status ⇒ Object
- #stream_events ⇒ Object
- #subdomains ⇒ Object
- #up! ⇒ Object
- #up? ⇒ Boolean
- #up_since ⇒ Object
- #update_config {|new_config| ... } ⇒ Object
- #validate ⇒ Object
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_names ⇒ Object
246 247 248 |
# File 'lib/stacco/stack.rb', line 246 def available_layer_names Stacco::Resources::LayerTemplates.keys end |
#available_layers ⇒ Object
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_credentials ⇒ Object
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_status ⇒ Object
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_operation ⇒ Object
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_template ⇒ Object
303 304 305 |
# File 'lib/stacco/stack.rb', line 303 def cloudformation_template Stacco::Template.const_get(self.config['template']).new end |
#cloudformation_template_body ⇒ Object
307 308 309 |
# File 'lib/stacco/stack.rb', line 307 def cloudformation_template_body self.cloudformation_template.to_json(stack: self) end |
#config ⇒ Object
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 |
#connections ⇒ Object
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.["aws:cloudformation:logical-id"]] = i } connections end |
#databases ⇒ Object
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 |
#description ⇒ Object
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 |
#domain ⇒ Object
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_names ⇒ Object
132 133 134 |
# File 'lib/stacco/stack.rb', line 132 def enabled_layer_names (self.available_layer_names & (self.config['layers'] || [])) end |
#enabled_layers ⇒ Object
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_name ⇒ Object
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_key ⇒ Object
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
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 |
#name ⇒ Object
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
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_summaries ⇒ Object
50 51 52 |
# File 'lib/stacco/stack.rb', line 50 def resource_summaries @aws_stack.resource_summaries end |
#roles ⇒ Object
242 243 244 |
# File 'lib/stacco/stack.rb', line 242 def roles Stacco::Resources::RoleScripts.keys end |
#secrets ⇒ Object
238 239 240 |
# File 'lib/stacco/stack.rb', line 238 def secrets self.config['secrets'] end |
#status ⇒ Object
79 80 81 |
# File 'lib/stacco/stack.rb', line 79 def status self.up? ? self.aws_status : "DOWN" end |
#stream_events ⇒ Object
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.['aws:cloudformation:logical-id']] if rs.['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. } 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 |
#subdomains ⇒ Object
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. =~ /no updates/i false end end |
#up? ⇒ Boolean
162 163 164 |
# File 'lib/stacco/stack.rb', line 162 def up? @aws_stack.exists? end |
#up_since ⇒ Object
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
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 |
#validate ⇒ Object
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. 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 |