Class: Hatchet::App

Inherits:
Object
  • Object
show all
Defined in:
lib/hatchet/app.rb

Direct Known Subclasses

AnvilApp, GitApp

Defined Under Namespace

Classes: FailedDeploy

Constant Summary collapse

HATCHET_BUILDPACK_BASE =
(ENV['HATCHET_BUILDPACK_BASE'] || "https://github.com/heroku/heroku-buildpack-ruby.git")
HATCHET_BUILDPACK_BRANCH =
-> { ENV['HATCHET_BUILDPACK_BRANCH'] || ENV['HEROKU_TEST_RUN_BRANCH'] || Hatchet.git_branch }
BUILDPACK_URL =
"https://github.com/heroku/heroku-buildpack-ruby.git"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(repo_name, stack: "", name: default_name, debug: nil, debugging: nil, allow_failure: false, labs: [], buildpack: nil, buildpacks: nil, buildpack_url: nil, before_deploy: nil, config: {}) ⇒ App

Returns a new instance of App.



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/hatchet/app.rb', line 24

def initialize(repo_name,
               stack: "",
               name: default_name,
               debug: nil,
               debugging: nil,
               allow_failure: false,
               labs: [],
               buildpack: nil,
               buildpacks: nil,
               buildpack_url: nil,
               before_deploy: nil,
               config: {}
              )
  @repo_name     = repo_name
  @directory     = self.config.path_for_name(@repo_name)
  @name          = name
  @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)
  @before_deploy = before_deploy
  @app_config    = config
  @reaper        = Reaper.new(api_rate_limit: api_rate_limit)
end

Instance Attribute Details

#directoryObject (readonly)

Returns the value of attribute directory.



12
13
14
# File 'lib/hatchet/app.rb', line 12

def directory
  @directory
end

#nameObject (readonly)

Returns the value of attribute name.



12
13
14
# File 'lib/hatchet/app.rb', line 12

def name
  @name
end

#repo_nameObject (readonly)

Returns the value of attribute repo_name.



12
13
14
# File 'lib/hatchet/app.rb', line 12

def repo_name
  @repo_name
end

#stackObject (readonly)

Returns the value of attribute stack.



12
13
14
# File 'lib/hatchet/app.rb', line 12

def stack
  @stack
end

Class Method Details

.configObject

config is read only, should be threadsafe



60
61
62
# File 'lib/hatchet/app.rb', line 60

def self.config
  @config ||= Config.new
end

.default_buildpackObject



51
52
53
# File 'lib/hatchet/app.rb', line 51

def self.default_buildpack
  [HATCHET_BUILDPACK_BASE, HATCHET_BUILDPACK_BRANCH.call].join("#")
end

Instance Method Details

#add_database(plan_name = 'heroku-postgresql:dev', match_val = "HEROKU_POSTGRESQL_[A-Z]+_URL") ⇒ Object



98
99
100
101
102
103
104
105
# File 'lib/hatchet/app.rb', line 98

def add_database(plan_name = 'heroku-postgresql:dev', match_val = "HEROKU_POSTGRESQL_[A-Z]+_URL")
  Hatchet::RETRIES.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

Returns:

  • (Boolean)


55
56
57
# File 'lib/hatchet/app.rb', line 55

def allow_failure?
  @allow_failure
end

#api_keyObject



257
258
259
# File 'lib/hatchet/app.rb', line 257

def api_key
  @api_key ||= ENV['HEROKU_API_KEY'] || bundle_exec {`heroku auth:token`.chomp }
end

#api_rate_limitObject



322
323
324
325
# File 'lib/hatchet/app.rb', line 322

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(&block) ⇒ Object



180
181
182
183
184
185
# File 'lib/hatchet/app.rb', line 180

def before_deploy(&block)
  raise "block required" unless block
  @before_deploy = block

  self
end

#commit!Object



187
188
189
# File 'lib/hatchet/app.rb', line 187

def commit!
  local_cmd_exec!('git add .; git commit -m next')
end

#configObject



64
65
66
# File 'lib/hatchet/app.rb', line 64

def config
  self.class.config
end

#create_appObject



144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/hatchet/app.rb', line 144

def create_app
  3.times.retry do
    begin
      # heroku.post_app({ name: name, stack: stack }.delete_if {|k,v| v.nil? })
      hash = { name: name, stack: stack }
      hash.delete_if { |k,v| v.nil? }
      api_rate_limit.call.app.create(hash)
    rescue => e
      @reaper.cycle
      raise e
    end
  end
end

#create_pipelineObject



294
295
296
# File 'lib/hatchet/app.rb', line 294

def create_pipeline
  api_rate_limit.call.pipeline.create(name: @name)
end

#create_sourceObject



303
304
305
306
307
308
309
310
# File 'lib/hatchet/app.rb', line 303

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

Returns:

  • (Boolean)


129
130
131
# File 'lib/hatchet/app.rb', line 129

def debug?
  @debug || ENV['HATCHET_DEBUG'] || false
end

#delete_pipeline(pipeline_id) ⇒ Object



312
313
314
# File 'lib/hatchet/app.rb', line 312

def delete_pipeline(pipeline_id)
  api_rate_limit.call.pipeline.delete(pipeline_id)
end

#deploy(&block) ⇒ Object

creates a new app on heroku, “pushes” via anvil or git then yields to self so you can call self.run or self.deployed? Allow deploy failures on CI server by setting ENV



221
222
223
224
225
226
227
228
229
# File 'lib/hatchet/app.rb', line 221

def deploy(&block)
  in_directory do
    self.setup!
    self.push_with_retry!
    block.call(self, api_rate_limit.call, output) if block_given?
  end
ensure
  self.teardown!
end

#deployed?Boolean

Returns:

  • (Boolean)


139
140
141
142
# File 'lib/hatchet/app.rb', line 139

def deployed?
  # !heroku.get_ps(name).body.detect {|ps| ps["process"].include?("web") }.nil?
  api_rate_limit.call.formation.list(name).detect {|ps| ps["type"] == "web"}
end

#get_configObject



75
76
77
78
# File 'lib/hatchet/app.rb', line 75

def get_config
  # heroku.get_config_vars(name).body
  api_rate_limit.call.config_var.info_for_app(name)
end

#get_labsObject



84
85
86
87
# File 'lib/hatchet/app.rb', line 84

def get_labs
  # heroku.get_features(name).body
  api_rate_limit.call.app_feature.list(name)
end

#herokuObject



261
262
263
# File 'lib/hatchet/app.rb', line 261

def heroku
  raise "Not supported, use `platform_api` instead."
end

#in_directory(directory = self.directory) ⇒ Object



204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/hatchet/app.rb', line 204

def in_directory(directory = self.directory)
  yield directory and return if @already_in_dir

  Dir.mktmpdir do |tmpdir|
    FileUtils.cp_r("#{directory}/.", "#{tmpdir}/.")
    Dir.chdir(tmpdir) do
      @already_in_dir = true
      yield directory
      @already_in_dir = false
    end
  end
end

#lab_is_installed?(lab) ⇒ Boolean

Returns:

  • (Boolean)


80
81
82
# File 'lib/hatchet/app.rb', line 80

def lab_is_installed?(lab)
  get_labs.any? {|hash| hash["name"] == lab }
end

#not_debugging?Boolean Also known as: no_debug?

Returns:

  • (Boolean)


134
135
136
# File 'lib/hatchet/app.rb', line 134

def not_debugging?
  !debug?
end

#outputObject



253
254
255
# File 'lib/hatchet/app.rb', line 253

def output
  @output
end

#pipeline_idObject



290
291
292
# File 'lib/hatchet/app.rb', line 290

def pipeline_id
  @pipeline_id
end

#platform_apiObject



316
317
318
319
320
# File 'lib/hatchet/app.rb', line 316

def platform_api
  puts "Deprecated: use `api_rate_limit.call` instead of platform_api"
  api_rate_limit
  return @platform_api
end

#pushObject Also known as: push!, push_with_retry



231
232
233
234
235
236
237
238
239
240
241
# File 'lib/hatchet/app.rb', line 231

def push
  max_retries = @allow_failure ? 1 : RETRIES
  max_retries.times.retry do |attempt|
    begin
      @output = self.push_without_retry!
    rescue StandardError => error
      puts retry_error_message(error, attempt, max_retries)
      raise error
    end
  end
end

#push_without_retry!Object

Raises:

  • (NotImplementedError)


191
192
193
# File 'lib/hatchet/app.rb', line 191

def push_without_retry!
  raise NotImplementedError
end

#retry_error_message(error, attempt, max_retries) ⇒ Object



246
247
248
249
250
251
# File 'lib/hatchet/app.rb', line 246

def retry_error_message(error, attempt, max_retries)
  attempt += 1
  return "" if attempt == max_retries
  msg = "\nRetrying failed Attempt ##{attempt}/#{max_retries} to push for '#{name}' due to error: \n"<<
        "#{error.class} #{error.message}\n  #{error.backtrace.join("\n  ")}"
end

#run(cmd_type, command = nil, options = {}, &block) ⇒ Object

runs a command on heroku similar to ‘$ heroku run #foo` but programatically and with more control



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/hatchet/app.rb', line 109

def run(cmd_type, command = nil, options = {}, &block)
  command        = cmd_type.to_s if command.nil?
  heroku_options = (options.delete(:heroku) || {}).map do |k,v|
    arg = "--#{k.to_s.shellescape}"
    arg << "=#{v.to_s.shellescape}" unless v.nil?
    arg
  end.join(" ")
  heroku_command = "heroku run -a #{name} #{heroku_options} --exit-code -- #{command}"
  bundle_exec do
    if block_given?
      ReplRunner.new(cmd_type, heroku_command, options).run(&block)
    else
      `#{heroku_command}`
    end
  end
end

#run_ci(timeout: 300, &block) ⇒ Object



265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
# File 'lib/hatchet/app.rb', line 265

def run_ci(timeout: 300, &block)
  Hatchet::RETRIES.times.retry do
    result       = create_pipeline
    @pipeline_id = result["id"]
  end

  # create_app
  # platform_api.pipeline_coupling.create(app: name, pipeline: @pipeline_id, stage: "development")
  test_run = TestRun.new(
    token:          api_key,
    buildpacks:     @buildpacks,
    timeout:        timeout,
    app:            self,
    pipeline:       @pipeline_id,
    api_rate_limit: api_rate_limit
 )

  Hatchet::RETRIES.times.retry do
    test_run.create_test_run
  end
  test_run.wait!(&block)
ensure
  delete_pipeline(@pipeline_id) if @pipeline_id
end

#set_config(options = {}) ⇒ Object



68
69
70
71
72
73
# File 'lib/hatchet/app.rb', line 68

def set_config(options = {})
  options.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



93
94
95
96
# File 'lib/hatchet/app.rb', line 93

def set_lab(lab)
  # heroku.post_feature(lab, name)
  api_rate_limit.call.app_feature.update(name, lab, enabled: true)
end

#set_labs!Object



89
90
91
# File 'lib/hatchet/app.rb', line 89

def set_labs!
  @labs.each {|lab| set_lab(lab) }
end

#setup!Object Also known as: setup

creates a new heroku app via the API



164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/hatchet/app.rb', line 164

def setup!
  return self if @app_is_setup
  puts "Hatchet setup: #{name.inspect} for #{repo_name.inspect}"
  create_git_repo! unless is_git_repo?
  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

  call_before_deploy
  @app_is_setup = true
  self
end

#source_get_urlObject



298
299
300
301
# File 'lib/hatchet/app.rb', line 298

def source_get_url
  create_source
  @source_get_url
end

#teardown!Object



195
196
197
198
199
200
201
202
# File 'lib/hatchet/app.rb', line 195

def teardown!
  return false unless @app_is_setup
  if debugging?
    puts "Debugging App:#{name}"
    return false
  end
  @reaper.cycle
end

#update_stack(stack_name) ⇒ Object



158
159
160
161
# File 'lib/hatchet/app.rb', line 158

def update_stack(stack_name)
  @stack = stack_name
  api_rate_limit.call.app.update(name, build_stack: @stack)
end