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"
SkipDefaultOption =
Object.new

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.



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

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



62
63
64
# File 'lib/hatchet/app.rb', line 62

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

.default_buildpackObject



53
54
55
# File 'lib/hatchet/app.rb', line 53

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



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

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)


57
58
59
# File 'lib/hatchet/app.rb', line 57

def allow_failure?
  @allow_failure
end

#api_keyObject



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

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

#api_rate_limitObject



326
327
328
329
# File 'lib/hatchet/app.rb', line 326

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



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

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

  self
end

#commit!Object



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

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

#configObject



66
67
68
# File 'lib/hatchet/app.rb', line 66

def config
  self.class.config
end

#create_appObject



148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/hatchet/app.rb', line 148

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



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

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

#create_sourceObject



307
308
309
310
311
312
313
314
# File 'lib/hatchet/app.rb', line 307

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)


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

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

#delete_pipeline(pipeline_id) ⇒ Object



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

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



225
226
227
228
229
230
231
232
233
# File 'lib/hatchet/app.rb', line 225

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)


143
144
145
146
# File 'lib/hatchet/app.rb', line 143

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



77
78
79
80
# File 'lib/hatchet/app.rb', line 77

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

#get_labsObject



86
87
88
89
# File 'lib/hatchet/app.rb', line 86

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

#herokuObject



265
266
267
# File 'lib/hatchet/app.rb', line 265

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

#in_directory(directory = self.directory) ⇒ Object



208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/hatchet/app.rb', line 208

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)


82
83
84
# File 'lib/hatchet/app.rb', line 82

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

#not_debugging?Boolean Also known as: no_debug?

Returns:

  • (Boolean)


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

def not_debugging?
  !debug?
end

#outputObject



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

def output
  @output
end

#pipeline_idObject



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

def pipeline_id
  @pipeline_id
end

#platform_apiObject



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

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



235
236
237
238
239
240
241
242
243
244
245
# File 'lib/hatchet/app.rb', line 235

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)


195
196
197
# File 'lib/hatchet/app.rb', line 195

def push_without_retry!
  raise NotImplementedError
end

#retry_error_message(error, attempt, max_retries) ⇒ Object



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

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



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

def run(cmd_type, command = nil, options = {}, &block)
  command        = cmd_type.to_s if command.nil?
  default_options = { "app" => name, "exit-code" => nil }
  heroku_options = (default_options.merge(options.delete(:heroku) || {})).map do |k,v|
    next if v == Hatchet::App::SkipDefaultOption # for forcefully removing e.g. --exit-code, a user can pass this
    arg = "--#{k.to_s.shellescape}"
    arg << "=#{v.to_s.shellescape}" unless v.nil? # nil means we include the option without an argument
    arg
  end.join(" ")
  heroku_command = "heroku run #{heroku_options} -- #{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



269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
# File 'lib/hatchet/app.rb', line 269

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



70
71
72
73
74
75
# File 'lib/hatchet/app.rb', line 70

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



95
96
97
98
# File 'lib/hatchet/app.rb', line 95

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

#set_labs!Object



91
92
93
# File 'lib/hatchet/app.rb', line 91

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

#setup!Object Also known as: setup

creates a new heroku app via the API



168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/hatchet/app.rb', line 168

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



302
303
304
305
# File 'lib/hatchet/app.rb', line 302

def source_get_url
  create_source
  @source_get_url
end

#teardown!Object



199
200
201
202
203
204
205
206
# File 'lib/hatchet/app.rb', line 199

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



162
163
164
165
# File 'lib/hatchet/app.rb', line 162

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