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'] || 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, options = {}) ⇒ App

Returns a new instance of App.



24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/hatchet/app.rb', line 24

def initialize(repo_name, options = {})
  @repo_name     = repo_name
  @directory     = config.path_for_name(@repo_name)
  @name          = options[:name]          || "hatchet-t-#{SecureRandom.hex(10)}"
  @stack         = options[:stack]
  @debug         = options[:debug]         || options[:debugging]
  @allow_failure = options[:allow_failure] || false
  @labs          = ([] << options[:labs]).flatten.compact
  @buildpacks    = options[:buildpack] || options[:buildpacks] || options[:buildpack_url] || self.class.default_buildpack
  @buildpacks    = Array(@buildpacks)
  @reaper        = Reaper.new(platform_api: platform_api)
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



46
47
48
# File 'lib/hatchet/app.rb', line 46

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

.default_buildpackObject



37
38
39
# File 'lib/hatchet/app.rb', line 37

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



84
85
86
87
88
89
90
91
# File 'lib/hatchet/app.rb', line 84

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)
    platform_api.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)


41
42
43
# File 'lib/hatchet/app.rb', line 41

def allow_failure?
  @allow_failure
end

#api_keyObject



223
224
225
# File 'lib/hatchet/app.rb', line 223

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

#configObject



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

def config
  self.class.config
end

#create_appObject



126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/hatchet/app.rb', line 126

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? }
      platform_api.app.create(hash)
    rescue
      @reaper.cycle
      raise e
    end
  end
end

#create_pipelineObject



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

def create_pipeline
  platform_api.pipeline.create(name: @name)
end

#create_sourceObject



266
267
268
269
270
271
272
273
# File 'lib/hatchet/app.rb', line 266

def create_source
  @create_source ||= begin
    result = platform_api.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)


111
112
113
# File 'lib/hatchet/app.rb', line 111

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

#delete_pipeline(pipeline_id) ⇒ Object



275
276
277
# File 'lib/hatchet/app.rb', line 275

def delete_pipeline(pipeline_id)
  platform_api.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



185
186
187
188
189
190
191
192
193
# File 'lib/hatchet/app.rb', line 185

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

#deployed?Boolean

Returns:

  • (Boolean)


121
122
123
124
# File 'lib/hatchet/app.rb', line 121

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

#get_configObject



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

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

#get_labsObject



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

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

#herokuObject



227
228
229
# File 'lib/hatchet/app.rb', line 227

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

#in_directory(directory = self.directory) ⇒ Object



172
173
174
175
176
177
178
179
# File 'lib/hatchet/app.rb', line 172

def in_directory(directory = self.directory)
  Dir.mktmpdir do |tmpdir|
    FileUtils.cp_r("#{directory}/.", "#{tmpdir}/.")
    Dir.chdir(tmpdir) do
      yield directory
    end
  end
end

#lab_is_installed?(lab) ⇒ Boolean

Returns:

  • (Boolean)


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

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

#not_debugging?Boolean Also known as: no_debug?

Returns:

  • (Boolean)


116
117
118
# File 'lib/hatchet/app.rb', line 116

def not_debugging?
  !debug?
end

#outputObject



219
220
221
# File 'lib/hatchet/app.rb', line 219

def output
  @output
end

#pipeline_idObject



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

def pipeline_id
  @pipeline_id
end

#platform_apiObject



279
280
281
282
# File 'lib/hatchet/app.rb', line 279

def platform_api
  # We have to not use a cache due to https://github.com/heroku/platform-api/issues/73
  @platform_api ||= PlatformAPI.connect_oauth(api_key, cache: Moneta.new(:Null))
end

#pushObject Also known as: push!, push_with_retry



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

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)


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

def push_without_retry!
  raise NotImplementedError
end

#retry_error_message(error, attempt, max_retries) ⇒ Object



212
213
214
215
216
217
# File 'lib/hatchet/app.rb', line 212

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



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

def run(cmd_type, command = nil, options = {}, &block)
  command        = cmd_type.to_s if command.nil?
  heroku_options = (options.delete(:heroku) || {}).map {|k,v| "--#{k.to_s.shellescape}=#{v.to_s.shellescape}"}.join(" ")
  heroku_command = "heroku run #{command.to_s.shellescape} -a #{name} #{ heroku_options }"
  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



231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/hatchet/app.rb', line 231

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)

  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



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

def set_config(options = {})
  options.each do |key, value|
    # heroku.put_config_vars(name, key => value)
    platform_api.config_var.update(name, key => value)
  end
end

#set_lab(lab) ⇒ Object



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

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

#set_labs!Object



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

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

#setup!Object Also known as: setup

creates a new heroku app via the API



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

def setup!
  return self if @app_is_setup
  puts "Hatchet setup: #{name.inspect} for #{repo_name.inspect}"
  create_app
  set_labs!
  # heroku.put_config_vars(name, 'BUILDPACK_URL' => @buildpack)
  buildpack_list = @buildpacks.map {|pack| { buildpack: pack }}
  platform_api.buildpack_installation.update(name, updates: buildpack_list)
  @app_is_setup = true
  self
end

#source_get_urlObject



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

def source_get_url
  create_source
  @source_get_url
end

#teardown!Object



163
164
165
166
167
168
169
170
# File 'lib/hatchet/app.rb', line 163

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



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

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