KentaaNL/capistrano-asg-rolling

View on GitHub
lib/capistrano/asg/rolling/instance.rb

Summary

Maintainability
A
35 mins
Test Coverage
# frozen_string_literal: true

require 'time'

module Capistrano
  module ASG
    module Rolling
      # AWS EC2 instance.
      class Instance
        include AWS

        attr_reader :id, :private_ip_address, :public_ip_address, :image_id, :autoscale_group
        attr_accessor :auto_terminate, :terminated
        alias auto_terminate? auto_terminate
        alias terminated? terminated

        def initialize(id, private_ip_address, public_ip_address, image_id, autoscale_group)
          @id = id
          @private_ip_address = private_ip_address
          @public_ip_address = public_ip_address
          @image_id = image_id
          @autoscale_group = autoscale_group
          @auto_terminate = true
          @terminated = false
        end

        # Launch a new instance based on settings from Auto Scaling Group and associated Launch Template.
        def self.run(autoscaling_group:, overrides: nil)
          launch_template = autoscaling_group.launch_template

          aws_ec2_client = autoscaling_group.aws_ec2_client
          options = {
            min_count: 1,
            max_count: 1,
            launch_template: {
              launch_template_id: launch_template.id,
              version: launch_template.version
            }
          }

          # Add a subnet defined in the Auto Scaling Group, but only when the Launch Template
          # does not define a network interface. Otherwise it will trigger the following error:
          # => Network interfaces and an instance-level subnet ID may not be specified on the same request
          options[:subnet_id] = autoscaling_group.subnet_ids.sample if launch_template.network_interfaces.empty?

          # Optionally override settings in the Launch Template.
          options.merge!(overrides) if overrides

          resource_tags = [
            { key: 'Name', value: autoscaling_group.name_tag }
          ]
          options[:tag_specifications] = [
            { resource_type: 'instance', tags: resource_tags },
            { resource_type: 'volume', tags: resource_tags }
          ]

          response = aws_ec2_client.run_instances(options)

          instance = response.instances.first

          # Wait until the instance is running and has a public IP address.
          aws_instance = ::Aws::EC2::Instance.new(instance.instance_id, client: aws_ec2_client)
          instance = aws_instance.wait_until_running

          new(instance.instance_id, instance.private_ip_address, instance.public_ip_address, instance.image_id, autoscaling_group)
        end

        def wait_for_ssh
          started_at = Time.now

          loop do
            result = SSH.test?(ip_address, autoscale_group.properties[:user], Configuration.ssh_options)

            break if result || Time.now - started_at > 300

            sleep 1
          end
        end

        def ip_address
          Configuration.use_private_ip_address? ? private_ip_address : public_ip_address
        end

        def start
          aws_ec2_client.start_instances(instance_ids: [id])
          aws_ec2_client.wait_until(:instance_running, instance_ids: [id])
        end

        def stop
          aws_ec2_client.stop_instances(instance_ids: [id])
          aws_ec2_client.wait_until(:instance_stopped, instance_ids: [id])
        end

        def terminate(wait: false)
          aws_ec2_client.terminate_instances(instance_ids: [id])
          aws_ec2_client.wait_until(:instance_terminated, instance_ids: [id]) if wait

          @terminated = true
        rescue Aws::EC2::Errors::ServiceError => e
          raise Capistrano::ASG::Rolling::InstanceTerminateFailed.new(self, e)
        end

        def create_ami(name: nil, description: nil, tags: nil)
          ami_tags = { 'Name' => autoscale_group.name_tag }
          ami_tags.merge!(tags) if tags

          AMI.create(instance: self, name: name || ami_name, description: description, tags: ami_tags)
        end

        private

        def ami_name
          "#{autoscale_group.name_tag} on #{Time.now.strftime('%Y-%m-%d at %H.%M.%S')}"
        end
      end
    end
  end
end