class Jets::Cfn::Ship

def api_gateway

It only gets called one more time so just let it get called.
Do not memoize this because on first stack run it will be nil
def api_gateway
  resp = cfn.describe_stack_resources(stack_name: @parent_stack_name)
  resources = resp.stack_resources
  resources.find { |resource| resource.logical_resource_id == "ApiGateway" }
end

def capabilities

def capabilities
  ["CAPABILITY_IAM", "CAPABILITY_NAMED_IAM"] # TODO: remove capabilities hardcode
  # return @options[:capabilities] if @options[:capabilities]
  # if @options[:iam]
  #   ["CAPABILITY_IAM", "CAPABILITY_NAMED_IAM"]
  # end
end

def clean_deploy_logs

def clean_deploy_logs
  Jets::Commands::Clean::Log.new.clean_deploys
end

def command_with_iam(capabilities)

def command_with_iam(capabilities)
  "#{File.basename($0)} #{ARGV.join(' ')} --capabilities #{capabilities}"
end

def create_stack

def create_stack
  # parent stack template is on filesystem and child stacks templates is on s3
  cfn.create_stack(stack_options)
end

def endpoint_available?

def endpoint_available?
  !endpoint_unavailable?
end

def endpoint_unavailable?

def endpoint_unavailable?
  return true unless @options[:stack_type] == :full # s3 bucket is available
  return true if Jets::Router.routes.empty?
  _, status = stack_status
  return true if status.include?("ROLLBACK")
  return true unless api_gateway
end

def initialize(options)

def initialize(options)
  @options = options
  @parent_stack_name = Jets::Naming.parent_stack_name
  @template_path = Jets::Naming.parent_template_path
end

def prewarm

def prewarm
  if ENV['SKIP_PREWARMING']
    puts "Skipping prewarming" # useful for testing
    return
  end
  return unless @options[:stack_type] == :full # s3 bucket is available
  return unless Jets.config.prewarm.enable
  return if Jets.poly_only?
  puts "Prewarming application."
  Jets::PreheatJob.prewarm!
end

def prompt_for_iam(capabilities)

def prompt_for_iam(capabilities)
  puts "This stack will create IAM resources.  Please approve to run the command again with #{capabilities} capabilities."
  puts "  #{command_with_iam(capabilities)}"
  puts "Please confirm (y/n)"
  $stdin.gets # confirm
end

def run

def run
  # s3 bucket is available only when stack_type is full
  upload_to_s3 if @options[:stack_type] == :full
  stack_in_progress?(@parent_stack_name)
  puts "Deploying CloudFormation stack with jets app!"
  begin
    save_stack
  rescue Aws::CloudFormation::Errors::InsufficientCapabilitiesException => e
    capabilities = e.message.match(/\[(.*)\]/)[1]
    confirm = prompt_for_iam(capabilities)
    if confirm =~ /^y/
      @options.merge!(capabilities: [capabilities])
      puts "Re-running: #{command_with_iam(capabilities).color(:green)}"
      retry
    else
      puts "Exited"
      exit 1
    end
  end
  success = wait_for_stack
  unless success
    puts <<~EOL
      The Jets application failed to deploy. Jets creates a few CloudFormation stacks to deploy your application.
      The logs above show the CloudFormation parent stack events and points to the stack with the error.
      Please go to the CloudFormation console and look for the specific stack with the error.
      The specific child stack usually shows more detailed information and can be used to resolve the issue.
      Example of checking the CloudFormation console: https://rubyonjets.com/docs/debugging/cloudformation/
    EOL
    return
  end
  prewarm
  clean_deploy_logs
  show_api_endpoint
  show_custom_domain
end

def save_stack

def save_stack
  if stack_exists?(@parent_stack_name)
    update_stack
  else
    create_stack
  end
end

def show_api_endpoint

def show_api_endpoint
  return unless endpoint_available?
  stack_id = api_gateway["physical_resource_id"]
  resp = cfn.describe_stacks(stack_name: stack_id)
  stack = resp["stacks"].first
  output = stack["outputs"].find { |o| o["output_key"] == "RestApiUrl" }
  endpoint = output["output_value"]
  puts "API Gateway Endpoint: #{endpoint}"
end

def show_custom_domain

def show_custom_domain
  return unless endpoint_available? && Jets.custom_domain? && Jets.config.domain.route53
  domain_name = Jets::Resource::ApiGateway::DomainName.new
  # Looks funny but its right.
  # domain_name is a method on the Jets::Resource::ApiGateway::Domain instance
  url = "https://#{domain_name.domain_name}"
  puts "Custom Domain: #{url}"
end

def stack_options

options common to both create_stack and update_stack
def stack_options
  {
    stack_name: @parent_stack_name,
    template_body: IO.read(@template_path),
    capabilities: capabilities, # ["CAPABILITY_IAM", "CAPABILITY_NAMED_IAM"]
    # disable_rollback: !@options[:rollback],
  }
end

def stack_status

http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-describing-stacks.html
All CloudFormation states listed here:
def stack_status
  resp = cfn.describe_stacks(stack_name: @parent_stack_name)
  status = resp.stacks[0].stack_status
  [resp, status]
end

def update_stack

def update_stack
  begin
    cfn.update_stack(stack_options)
  rescue Aws::CloudFormation::Errors::ValidationError => e
    puts "ERROR: #{e.message}".color(:red)
    true # error
  end
end

def upload_to_s3

Upload both code and child templates to s3
def upload_to_s3
  raise "Did not specify @options[:s3_bucket] #{@options[:s3_bucket].inspect}" unless @options[:s3_bucket]
  uploader = Upload.new(@options[:s3_bucket])
  uploader.upload
end

def wait_for_stack

check for /(_COMPLETE|_FAILED)$/ status
def wait_for_stack
  Jets::Cfn::Status.new(@options).wait
end