module Nucleus
module Adapters
module V1
# The {Heroku} adapter is designed to support the Heroku platform API.
#
# The Nucleus API is fully supported, there are no known issues.
# @see https://devcenter.heroku.com/articles/platform-api-reference Heroku Platform API
class Heroku < Stub
include Nucleus::Logging
include Nucleus::Adapters::V1::Heroku::Authentication
include Nucleus::Adapters::V1::Heroku::Application
include Nucleus::Adapters::V1::Heroku::AppStates
include Nucleus::Adapters::V1::Heroku::Buildpacks
include Nucleus::Adapters::V1::Heroku::Data
include Nucleus::Adapters::V1::Heroku::Domains
include Nucleus::Adapters::V1::Heroku::Logs
include Nucleus::Adapters::V1::Heroku::Lifecycle
include Nucleus::Adapters::V1::Heroku::Regions
include Nucleus::Adapters::V1::Heroku::Scaling
include Nucleus::Adapters::V1::Heroku::Services
include Nucleus::Adapters::V1::Heroku::SemanticErrors
include Nucleus::Adapters::V1::Heroku::Vars
def initialize(endpoint_url, endpoint_app_domain = nil, check_certificates = true)
super(endpoint_url, endpoint_app_domain, check_certificates)
end
def handle_error(error_response)
handle_422(error_response)
if error_response.status == 404 && error_response.body[:id] == 'not_found'
raise Errors::AdapterResourceNotFoundError, error_response.body[:message]
elsif error_response.status == 503
raise Errors::PlatformUnavailableError, 'The Heroku API is currently not responding'
end
# error still unhandled, will result in a 500, server error
log.warn "Heroku error still unhandled: #{error_response}"
end
def handle_422(error_response)
return unless error_response.status == 422
if error_response.body[:id] == 'invalid_params'
raise Errors::SemanticAdapterRequestError, error_response.body[:message]
elsif error_response.body[:id] == 'verification_required'
fail_with(:need_verification, [error_response.body[:message]])
end
end
private
def install_runtimes(application_id, runtimes)
runtime_instructions = runtimes.collect { |buildpack_url| { buildpack: buildpack_url } }
log.debug "Install runtimes: #{runtime_instructions}"
buildpack_instructions = { updates: runtime_instructions }
put("/apps/#{application_id}/buildpack-installations", body: buildpack_instructions)
end
def runtimes_to_install(application)
return [] unless application[:runtimes]
runtimes_to_install = []
application[:runtimes].each do |runtime_identifier|
# we do not need to install native buildpacks
# TODO: 2 options for heroku runtime handling
# a) skip native, fails when native required and not in list
# b) (current) use native, fails when others (additional) are in the list
# next if native_runtime?(runtime_identifier)
runtime_is_url = runtime_identifier =~ /\A#{URI.regexp}\z/
runtime_url = find_runtime(runtime_identifier)
runtime_is_valid = runtime_url || runtime_is_url
fail_with(:invalid_runtime, [runtime_identifier]) unless runtime_is_valid
# if runtime identifier is valid, we need to install the runtime
runtimes_to_install.push(runtime_is_url ? runtime_identifier : runtime_url)
end
# heroku does not know the 'runtimes' property and would crash if present
application.delete :runtimes
runtimes_to_install
end
def heroku_api
::Heroku::API.new(headers: headers)
end
def headers
super.merge(
'Accept' => 'application/vnd.heroku+json; version=3',
'Content-Type' => 'application/json'
)
end
def installed_buildpacks(application_id)
buildpacks = get("/apps/#{application_id}/buildpack-installations").body
return [] if buildpacks.empty?
buildpacks.collect do |buildpack|
buildpack[:buildpack][:url]
end
end
def application_instances(application_id)
formations = get("/apps/#{application_id}/formation").body
web_formation = formations.find { |formation| formation[:type] == 'web' }
return web_formation[:quantity] unless web_formation.nil?
# if no web formation was detected, there is no instance available
0
end
def dynos(application_id)
get("/apps/#{application_id}/dynos").body
end
def web_dynos(application_id, retrieved_dynos = nil)
all_dynos = retrieved_dynos ? retrieved_dynos : dynos(application_id)
all_dynos.find_all do |dyno|
dyno[:type] == 'web'
end.compact
end
def latest_release(application_id, retrieved_dynos = nil)
dynos = web_dynos(application_id, retrieved_dynos)
if dynos.nil? || dynos.empty?
log.debug 'no dynos for build detection, fallback to latest release version'
# this approach might be wrong if the app is rolled-back to a previous release
# However, if no dyno is active, this is the only option to identify the current release
latest_version = 0
latest_version_id = nil
get("/apps/#{application_id}/releases").body.each do |release|
if release[:version] > latest_version
latest_version = release[:version]
latest_version_id = release[:id]
end
end
else
latest_version = 0
latest_version_id = nil
dynos.each do |dyno|
if dyno[:release][:version] > latest_version
latest_version = dyno[:release][:version]
latest_version_id = dyno[:release][:id]
end
end
end
latest_version_id
end
end
end
end
end