Class: Hatchet::App
- Inherits:
-
Object
- Object
- Hatchet::App
- Defined in:
- lib/hatchet/app.rb
Defined Under Namespace
Classes: FailedDeploy, FailedDeployError, FailedReleaseError
Constant Summary collapse
- HATCHET_BUILDPACK_BASE =
-> { ENV.fetch('HATCHET_BUILDPACK_BASE') { warn "ENV HATCHET_BUILDPACK_BASE is not set. It currently defaults to the ruby buildpack. In the future this env var will be required" "https://github.com/heroku/heroku-buildpack-ruby.git" } }
- HATCHET_BUILDPACK_BRANCH =
-> { ENV['HATCHET_BUILDPACK_BRANCH'] || Hatchet.git_branch }
- SkipDefaultOption =
Object.new
- DEFAULT_REPO_NAME =
Object.new
- DefaultCommand =
Object.new
Instance Attribute Summary collapse
-
#app_config ⇒ Object
readonly
Returns the value of attribute app_config.
-
#buildpacks ⇒ Object
readonly
Returns the value of attribute buildpacks.
-
#max_retries_count ⇒ Object
readonly
Returns the value of attribute max_retries_count.
-
#name ⇒ Object
readonly
Returns the value of attribute name.
-
#reaper ⇒ Object
readonly
Returns the value of attribute reaper.
-
#repo_name ⇒ Object
readonly
Returns the value of attribute repo_name.
-
#stack ⇒ Object
readonly
Returns the value of attribute stack.
Class Method Summary collapse
-
.config ⇒ Object
config is read only, should be threadsafe.
- .default_buildpack ⇒ Object
Instance Method Summary collapse
- #add_database(plan_name = 'heroku-postgresql:dev', match_val = "HEROKU_POSTGRESQL_[A-Z]+_URL") ⇒ Object
- #allow_failure? ⇒ Boolean
- #annotate_failures ⇒ Object
- #api_key ⇒ Object
- #api_rate_limit ⇒ Object
- #before_deploy(behavior = :default, &block) ⇒ Object
- #commit! ⇒ Object
- #config ⇒ Object
- #couple_pipeline(app_name, pipeline_id) ⇒ Object
- #create_app ⇒ Object
- #create_pipeline ⇒ Object
- #create_source ⇒ Object
-
#debug? ⇒ Boolean
(also: #debugging?)
set debug: true when creating app if you don’t want it to be automatically destroyed, useful for debugging…bad for app limits.
- #delete_pipeline(pipeline_id) ⇒ Object
- #deploy(&block) ⇒ Object
- #deployed? ⇒ Boolean
- #directory ⇒ Object
- #get_config ⇒ Object
- #get_labs ⇒ Object
- #heroku ⇒ Object
- #in_directory ⇒ Object
-
#in_directory_fork(&block) ⇒ Object
A safer alternative to in_directory this method is used to run code that may mutate the current process anything run in this block is executed in a different fork.
-
#initialize(repo_name = DEFAULT_REPO_NAME, stack: ENV["HATCHET_DEFAULT_STACK"], name: default_name, debug: nil, debugging: nil, allow_failure: false, labs: [], buildpack: nil, buildpacks: nil, buildpack_url: nil, before_deploy: nil, run_multi: , retries: RETRIES, config: {}) ⇒ App
constructor
A new instance of App.
- #lab_is_installed?(lab) ⇒ Boolean
- #not_debugging? ⇒ Boolean (also: #no_debug?)
- #original_source_code_directory ⇒ Object
- #output ⇒ Object
- #pipeline_id ⇒ Object
- #platform_api ⇒ Object
- #push ⇒ Object (also: #push!, #push_with_retry)
- #push_without_retry! ⇒ Object
- #retry_error_message(error, attempt) ⇒ Object
-
#run(cmd_type, command = DefaultCommand, options = {}, &block) ⇒ Object
runs a command on heroku similar to ‘$ heroku run #foo` but programatically and with more control.
- #run_ci(timeout: 900, &block) ⇒ Object
-
#run_multi(command, options = {}, &block) ⇒ Object
Allows multiple commands to be run concurrently in the background.
- #set_config(options = {}) ⇒ Object
- #set_lab(lab) ⇒ Object
- #set_labs! ⇒ Object
-
#setup! ⇒ Object
(also: #setup)
creates a new heroku app via the API.
- #source_get_url ⇒ Object
- #teardown! ⇒ Object
- #update_stack(stack_name) ⇒ Object
Constructor Details
#initialize(repo_name = DEFAULT_REPO_NAME, stack: ENV["HATCHET_DEFAULT_STACK"], name: default_name, debug: nil, debugging: nil, allow_failure: false, labs: [], buildpack: nil, buildpacks: nil, buildpack_url: nil, before_deploy: nil, run_multi: , retries: RETRIES, config: {}) ⇒ App
Returns a new instance of App.
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'lib/hatchet/app.rb', line 51 def initialize(repo_name = DEFAULT_REPO_NAME, stack: ENV["HATCHET_DEFAULT_STACK"], name: default_name, debug: nil, debugging: nil, allow_failure: false, labs: [], buildpack: nil, buildpacks: nil, buildpack_url: nil, before_deploy: nil, run_multi: ENV["HATCHET_RUN_MULTI"], retries: RETRIES, config: {} ) raise "You tried creating a Hatchet::App instance without source code, pass in a path to an app to deploy or the name of an app in your hatchet.json" if repo_name == DEFAULT_REPO_NAME @repo_name = repo_name @directory = self.config.path_for_name(@repo_name) @name = name @heroku_id = nil @stack = stack @debug = debug || debugging @allow_failure = allow_failure @labs = ([] << labs).flatten.compact @buildpacks = buildpack || buildpacks || buildpack_url || self.class.default_buildpack @buildpacks = Array(@buildpacks) @buildpacks.map! {|b| b == :default ? self.class.default_buildpack : b} @run_multi = run_multi @max_retries_count = retries @outer_deploy_block = nil if run_multi && !ENV["HATCHET_EXPENSIVE_MODE"] raise "You're attempting to enable `run_multi: true` mode, but have not enabled `HATCHET_EXPENSIVE_MODE=1` env var to verify you understand the risks" end @run_multi_array = [] @already_in_dir = nil @before_deploy_array = [] @before_deploy_array << before_deploy if before_deploy @app_config = config @reaper = Reaper.new(api_rate_limit: api_rate_limit) end |
Instance Attribute Details
#app_config ⇒ Object (readonly)
Returns the value of attribute app_config.
16 17 18 |
# File 'lib/hatchet/app.rb', line 16 def app_config @app_config end |
#buildpacks ⇒ Object (readonly)
Returns the value of attribute buildpacks.
16 17 18 |
# File 'lib/hatchet/app.rb', line 16 def buildpacks @buildpacks end |
#max_retries_count ⇒ Object (readonly)
Returns the value of attribute max_retries_count.
16 17 18 |
# File 'lib/hatchet/app.rb', line 16 def max_retries_count @max_retries_count end |
#name ⇒ Object (readonly)
Returns the value of attribute name.
16 17 18 |
# File 'lib/hatchet/app.rb', line 16 def name @name end |
#reaper ⇒ Object (readonly)
Returns the value of attribute reaper.
16 17 18 |
# File 'lib/hatchet/app.rb', line 16 def reaper @reaper end |
#repo_name ⇒ Object (readonly)
Returns the value of attribute repo_name.
16 17 18 |
# File 'lib/hatchet/app.rb', line 16 def repo_name @repo_name end |
#stack ⇒ Object (readonly)
Returns the value of attribute stack.
16 17 18 |
# File 'lib/hatchet/app.rb', line 16 def stack @stack end |
Class Method Details
.config ⇒ Object
config is read only, should be threadsafe
126 127 128 |
# File 'lib/hatchet/app.rb', line 126 def self.config @config ||= Config.new end |
.default_buildpack ⇒ Object
117 118 119 |
# File 'lib/hatchet/app.rb', line 117 def self.default_buildpack @default_buildpack ||= [HATCHET_BUILDPACK_BASE.call, HATCHET_BUILDPACK_BRANCH.call].join("#") end |
Instance Method Details
#add_database(plan_name = 'heroku-postgresql:dev', match_val = "HEROKU_POSTGRESQL_[A-Z]+_URL") ⇒ Object
164 165 166 167 168 169 170 171 |
# File 'lib/hatchet/app.rb', line 164 def add_database(plan_name = 'heroku-postgresql:dev', match_val = "HEROKU_POSTGRESQL_[A-Z]+_URL") max_retries_count.times.retry do # heroku.post_addon(name, plan_name) api_rate_limit.call.addon.create(name, plan: plan_name ) _, value = get_config.detect {|k, v| k.match(/#{match_val}/) } set_config('DATABASE_URL' => value) end end |
#allow_failure? ⇒ Boolean
121 122 123 |
# File 'lib/hatchet/app.rb', line 121 def allow_failure? @allow_failure end |
#annotate_failures ⇒ Object
100 101 102 103 104 |
# File 'lib/hatchet/app.rb', line 100 def annotate_failures yield rescue *test_failure_classes => e raise e, "App: #{name} (#{@repo_name})\n#{e.}" end |
#api_key ⇒ Object
465 466 467 |
# File 'lib/hatchet/app.rb', line 465 def api_key @api_key ||= ENV['HEROKU_API_KEY'] ||= `heroku auth:token 2> /dev/null`.chomp end |
#api_rate_limit ⇒ Object
548 549 550 551 |
# File 'lib/hatchet/app.rb', line 548 def api_rate_limit @platform_api ||= PlatformAPI.connect_oauth(api_key, cache: Moneta.new(:Null)) @api_rate_limit ||= ApiRateLimit.new(@platform_api) end |
#before_deploy(behavior = :default, &block) ⇒ Object
347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 |
# File 'lib/hatchet/app.rb', line 347 def before_deploy(behavior = :default, &block) raise "block required" unless block case behavior when :default, :replace if @before_deploy_array.any? && behavior == :default STDERR.puts "Calling App#before_deploy multiple times will overwrite the contents. If you intended this: use `App#before_deploy(:replace)`" STDERR.puts "In the future, calling this method with no arguements will default to `App#before_deploy(:append)` behavior.\n#{caller.join("\n")}" end @before_deploy_array.clear @before_deploy_array << block when :prepend @before_deploy_array = [block] + @before_deploy_array when :append @before_deploy_array << block else raise "Unrecognized behavior: #{behavior.inspect}, valid inputs are :append, :prepend, and :replace" end self end |
#commit! ⇒ Object
370 371 372 |
# File 'lib/hatchet/app.rb', line 370 def commit! local_cmd_exec!('git add .; git commit --allow-empty -m next') end |
#config ⇒ Object
130 131 132 |
# File 'lib/hatchet/app.rb', line 130 def config self.class.config end |
#couple_pipeline(app_name, pipeline_id) ⇒ Object
518 519 520 |
# File 'lib/hatchet/app.rb', line 518 def couple_pipeline(app_name, pipeline_id) api_rate_limit.call.pipeline_coupling.create(app: app_name, pipeline: pipeline_id, stage: "development") end |
#create_app ⇒ Object
281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 |
# File 'lib/hatchet/app.rb', line 281 def create_app 3.times.retry do begin # Remove any obviously old apps first # Try to use existing cache of apps to # minimize API calls @reaper.destroy_older_apps( force_refresh: false, on_conflict: :stop_if_under_limit, ) hash = { name: name, stack: stack } hash.delete_if { |k,v| v.nil? } result = heroku_api_create_app(hash) @heroku_id = result["id"] rescue => e # If we can't create an app assume # it might be due to resource constraints # # Try to delete existing apps @reaper.destroy_older_apps( force_refresh: true, on_conflict: :stop_if_under_limit, ) # If we're still not under the limit, sleep a bit # retry later. @reaper.sleep_if_over_limit( reason: "Could not create app #{e.}" ) raise e end end end |
#create_pipeline ⇒ Object
514 515 516 |
# File 'lib/hatchet/app.rb', line 514 def create_pipeline api_rate_limit.call.pipeline.create(name: @name) end |
#create_source ⇒ Object
527 528 529 530 531 532 533 534 |
# File 'lib/hatchet/app.rb', line 527 def create_source @create_source ||= begin result = api_rate_limit.call.source.create @source_get_url = result["source_blob"]["get_url"] @source_put_url = result["source_blob"]["put_url"] @source_put_url end end |
#debug? ⇒ Boolean Also known as: debugging?
set debug: true when creating app if you don’t want it to be automatically destroyed, useful for debugging…bad for app limits. turn on global debug by setting HATCHET_DEBUG=true in the env
267 268 269 |
# File 'lib/hatchet/app.rb', line 267 def debug? @debug || ENV['HATCHET_DEBUG'] || false end |
#delete_pipeline(pipeline_id) ⇒ Object
536 537 538 539 540 541 |
# File 'lib/hatchet/app.rb', line 536 def delete_pipeline(pipeline_id) api_rate_limit.call.pipeline.delete(pipeline_id) rescue Excon::Error::Forbidden warn "Error deleting pipeline id: #{pipeline_id.inspect}, status: 403" # Means the pipeline likely doesn't exist, not sure why though end |
#deploy(&block) ⇒ Object
425 426 427 428 429 430 431 432 433 434 435 436 |
# File 'lib/hatchet/app.rb', line 425 def deploy(&block) in_directory do annotate_failures do @outer_deploy_block ||= block # deploy! can be called multiple times. Only teardown once in_dir_setup! push_with_retry! block.call(self, api_rate_limit.call, output) if block_given? end end ensure self.teardown! if block_given? && @outer_deploy_block == block end |
#deployed? ⇒ Boolean
277 278 279 |
# File 'lib/hatchet/app.rb', line 277 def deployed? api_rate_limit.call.formation.list(name).detect {|ps| ps["type"] == "web"} end |
#directory ⇒ Object
106 107 108 109 110 |
# File 'lib/hatchet/app.rb', line 106 def directory warn "Calling App#directory returns the original location of the app's source code that should not be modified, if this is really what you want use `original_source_code_directory` instead." warn caller @directory end |
#get_config ⇒ Object
141 142 143 144 |
# File 'lib/hatchet/app.rb', line 141 def get_config # heroku.get_config_vars(name).body api_rate_limit.call.config_var.info_for_app(name) end |
#get_labs ⇒ Object
150 151 152 153 |
# File 'lib/hatchet/app.rb', line 150 def get_labs # heroku.get_features(name).body api_rate_limit.call.app_feature.list(name) end |
#heroku ⇒ Object
469 470 471 |
# File 'lib/hatchet/app.rb', line 469 def heroku raise "Not supported, use `platform_api` instead." end |
#in_directory ⇒ Object
386 387 388 389 390 391 392 393 394 395 396 397 |
# File 'lib/hatchet/app.rb', line 386 def in_directory yield and return if @already_in_dir Dir.mktmpdir do |tmpdir| FileUtils.cp_r("#{original_source_code_directory}/.", "#{tmpdir}/.") Dir.chdir(tmpdir) do @already_in_dir = true yield @already_in_dir = false end end end |
#in_directory_fork(&block) ⇒ Object
A safer alternative to in_directory this method is used to run code that may mutate the current process anything run in this block is executed in a different fork
403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 |
# File 'lib/hatchet/app.rb', line 403 def in_directory_fork(&block) Tempfile.create("stdout") do |tmp_file| pid = fork do $stdout.reopen(tmp_file, "a") $stderr.reopen(tmp_file, "a") $stdout.sync = true $stderr.sync = true in_directory do |dir| yield dir end Kernel.exit!(0) # needed for https://github.com/seattlerb/minitest/pull/683 end Process.waitpid(pid) if $?.success? print File.read(tmp_file) else raise File.read(tmp_file) end end end |
#lab_is_installed?(lab) ⇒ Boolean
146 147 148 |
# File 'lib/hatchet/app.rb', line 146 def lab_is_installed?(lab) get_labs.any? {|hash| hash["name"] == lab } end |
#not_debugging? ⇒ Boolean Also known as: no_debug?
272 273 274 |
# File 'lib/hatchet/app.rb', line 272 def not_debugging? !debug? end |
#original_source_code_directory ⇒ Object
112 113 114 |
# File 'lib/hatchet/app.rb', line 112 def original_source_code_directory @directory end |
#output ⇒ Object
461 462 463 |
# File 'lib/hatchet/app.rb', line 461 def output @output end |
#pipeline_id ⇒ Object
510 511 512 |
# File 'lib/hatchet/app.rb', line 510 def pipeline_id @pipeline_id end |
#platform_api ⇒ Object
543 544 545 546 |
# File 'lib/hatchet/app.rb', line 543 def platform_api api_rate_limit return @platform_api end |
#push ⇒ Object Also known as: push!, push_with_retry
438 439 440 441 442 443 444 445 446 447 448 |
# File 'lib/hatchet/app.rb', line 438 def push retry_count = allow_failure? ? 1 : max_retries_count retry_count.times.retry do |attempt| begin @output = self.push_without_retry! rescue StandardError => error puts (error, attempt) unless retry_count == 1 raise error end end end |
#push_without_retry! ⇒ Object
374 375 376 |
# File 'lib/hatchet/app.rb', line 374 def push_without_retry! raise NotImplementedError end |
#retry_error_message(error, attempt) ⇒ Object
453 454 455 456 457 458 459 |
# File 'lib/hatchet/app.rb', line 453 def (error, attempt) attempt += 1 return "" if attempt == max_retries_count msg = "\nRetrying failed Attempt ##{attempt}/#{max_retries_count} to push for '#{name}' due to error: \n"<< "#{error.class} #{error.}\n #{error.backtrace.join("\n ")}" return msg end |
#run(cmd_type, command = DefaultCommand, options = {}, &block) ⇒ Object
runs a command on heroku similar to ‘$ heroku run #foo` but programatically and with more control
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 |
# File 'lib/hatchet/app.rb', line 176 def run(cmd_type, command = DefaultCommand, = {}, &block) case command when Hash .merge!(command) command = cmd_type.to_s when nil STDERR.puts "Calling App#run with an explicit nil value in the second argument is deprecated." STDERR.puts "You can pass in a hash directly as the second argument now.\n#{caller.join("\n")}" command = cmd_type.to_s when DefaultCommand command = cmd_type.to_s else command = command.to_s end allow_run_multi! if @run_multi run_obj = Hatchet::HerokuRun.new( command, app: self, retry_on_empty: .fetch(:retry_on_empty, !ENV["HATCHET_DISABLE_EMPTY_RUN_RETRY"]), heroku: [:heroku], raw: [:raw] ).call return run_obj.output end |
#run_ci(timeout: 900, &block) ⇒ Object
473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 |
# File 'lib/hatchet/app.rb', line 473 def run_ci(timeout: 900, &block) in_directory do max_retries_count.times.retry do result = create_pipeline @pipeline_id = result["id"] end # When the CI run finishes, the associated ephemeral app created for the test run internally gets removed almost immediately # the system then sees a pipeline with no apps, and deletes it, also almost immediately # that would, with bad timing, mean our test run info poll in wait! would 403, and/or the delete_pipeline at the end # that's why we create an app explictly (or maybe it already exists), and then associate it with with the pipeline # the app will be auto cleaned up later in_dir_setup! max_retries_count.times.retry do couple_pipeline(@name, @pipeline_id) end test_run = TestRun.new( token: api_key, buildpacks: @buildpacks, timeout: timeout, app: self, pipeline: @pipeline_id, api_rate_limit: api_rate_limit ) max_retries_count.times.retry do test_run.create_test_run end test_run.wait!(&block) end ensure teardown! if block_given? delete_pipeline(@pipeline_id) if @pipeline_id @pipeline_id = nil end |
#run_multi(command, options = {}, &block) ⇒ Object
Allows multiple commands to be run concurrently in the background.
WARNING! Using the feature requres that the underlying app is not on the “free” Heroku tier. This requires scaling up the dyno which is not free. If an app is scaled up and left in that state it can incur large costs.
Enabling this feature should be done with extreme caution.
Example:
Hatchet::Runner.new("default_ruby", run_multi: true)
app.run_multi("ls") { |out| expect(out).to include("Gemfile") }
app.run_multi("ruby -v") { |out| expect(out).to include("ruby") }
end
This example will run ‘heroku run ls` as well as `ruby -v` at the same time in the background. The return result will be yielded to the block after they finish running.
Order of execution is not guaranteed.
If you need to assert a command was successful, you can yield a second status object like this:
Hatchet::Runner.new("default_ruby", run_multi: true)
app.run_multi("ls") do |out, status|
expect(status.success?).to be_truthy
expect(out).to include("Gemfile")
end
app.run_multi("ruby -v") do |out, status|
expect(status.success?).to be_truthy
expect(out).to include("ruby")
end
end
242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 |
# File 'lib/hatchet/app.rb', line 242 def run_multi(command, = {}, &block) raise "Block required" if block.nil? allow_run_multi! run_thread = Thread.new do run_obj = Hatchet::HerokuRun.new( command, app: self, retry_on_empty: .fetch(:retry_on_empty, !ENV["HATCHET_DISABLE_EMPTY_RUN_RETRY"]), heroku: [:heroku], raw: [:raw] ).call yield run_obj.output, run_obj.status end run_thread.abort_on_exception = true @run_multi_array << run_thread true end |
#set_config(options = {}) ⇒ Object
134 135 136 137 138 139 |
# File 'lib/hatchet/app.rb', line 134 def set_config( = {}) .each do |key, value| # heroku.put_config_vars(name, key => value) api_rate_limit.call.config_var.update(name, key => value) end end |
#set_lab(lab) ⇒ Object
159 160 161 162 |
# File 'lib/hatchet/app.rb', line 159 def set_lab(lab) # heroku.post_feature(lab, name) api_rate_limit.call.app_feature.update(name, lab, enabled: true) end |
#set_labs! ⇒ Object
155 156 157 |
# File 'lib/hatchet/app.rb', line 155 def set_labs! @labs.each {|lab| set_lab(lab) } end |
#setup! ⇒ Object Also known as: setup
creates a new heroku app via the API
324 325 326 327 328 329 330 331 332 333 334 |
# File 'lib/hatchet/app.rb', line 324 def setup! return self if @heroku_id puts "Hatchet setup: #{name.inspect} for #{repo_name.inspect}" create_app set_labs! buildpack_list = @buildpacks.map { |pack| { buildpack: pack } } api_rate_limit.call.buildpack_installation.update(name, updates: buildpack_list) set_config @app_config self end |
#source_get_url ⇒ Object
522 523 524 525 |
# File 'lib/hatchet/app.rb', line 522 def source_get_url create_source @source_get_url end |
#teardown! ⇒ Object
378 379 380 381 382 383 384 |
# File 'lib/hatchet/app.rb', line 378 def teardown! @run_multi_array.map(&:join) ensure if @heroku_id && !ENV["HEROKU_DEBUG_EXPENSIVE"] @reaper.destroy_with_log(name: @name, id: @heroku_id, reason: "teardown") end end |
#update_stack(stack_name) ⇒ Object
318 319 320 321 |
# File 'lib/hatchet/app.rb', line 318 def update_stack(stack_name) @stack = stack_name api_rate_limit.call.app.update(name, build_stack: @stack) end |