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.



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

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
  @buildpack     = options[:buildpack] || options[:buildpack_url] || [HATCHET_BUILDPACK_BASE, HATCHET_BUILDPACK_BRANCH.call].join("#")
  @reaper        = Reaper.new(heroku)
end

Instance Attribute Details

#directoryObject (readonly)

Returns the value of attribute directory.



10
11
12
# File 'lib/hatchet/app.rb', line 10

def directory
  @directory
end

#nameObject (readonly)

Returns the value of attribute name.



10
11
12
# File 'lib/hatchet/app.rb', line 10

def name
  @name
end

#repo_nameObject (readonly)

Returns the value of attribute repo_name.



10
11
12
# File 'lib/hatchet/app.rb', line 10

def repo_name
  @repo_name
end

#stackObject (readonly)

Returns the value of attribute stack.



10
11
12
# File 'lib/hatchet/app.rb', line 10

def stack
  @stack
end

Class Method Details

.configObject

config is read only, should be threadsafe



39
40
41
# File 'lib/hatchet/app.rb', line 39

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

Instance Method Details

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



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

def add_database(db_name = 'heroku-postgresql:dev', match_val = "HEROKU_POSTGRESQL_[A-Z]+_URL")
  Hatchet::RETRIES.times.retry do
    heroku.post_addon(name, db_name)
    _, value = get_config.detect {|k, v| k.match(/#{match_val}/) }
    set_config('DATABASE_URL' => value)
  end
end

#allow_failure?Boolean

Returns:

  • (Boolean)


34
35
36
# File 'lib/hatchet/app.rb', line 34

def allow_failure?
  @allow_failure
end

#api_keyObject



199
200
201
# File 'lib/hatchet/app.rb', line 199

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

#configObject



43
44
45
# File 'lib/hatchet/app.rb', line 43

def config
  self.class.config
end

#create_appObject



113
114
115
116
117
118
119
120
121
122
# File 'lib/hatchet/app.rb', line 113

def create_app
  3.times.retry do
    begin
      heroku.post_app({ name: name, stack: stack }.delete_if {|k,v| v.nil? })
    rescue Heroku::API::Errors::RequestFailed => e
      @reaper.cycle if e.message.match(/app limit/)
      raise e
    end
  end
end

#create_pipelineObject



233
234
235
# File 'lib/hatchet/app.rb', line 233

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

#create_sourceObject



242
243
244
245
246
247
248
249
# File 'lib/hatchet/app.rb', line 242

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)


99
100
101
# File 'lib/hatchet/app.rb', line 99

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

#delete_pipeline(pipeline_id) ⇒ Object



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

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



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

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

#deployed?Boolean

Returns:

  • (Boolean)


109
110
111
# File 'lib/hatchet/app.rb', line 109

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

#get_configObject



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

def get_config
  heroku.get_config_vars(name).body
end

#get_labsObject



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

def get_labs
  heroku.get_features(name).body
end

#herokuObject



203
204
205
# File 'lib/hatchet/app.rb', line 203

def heroku
  @heroku ||= Heroku::API.new(api_key: api_key)
end

#in_directory(directory = self.directory) ⇒ Object



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

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)


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

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

#not_debugging?Boolean Also known as: no_debug?

Returns:

  • (Boolean)


104
105
106
# File 'lib/hatchet/app.rb', line 104

def not_debugging?
  !debug?
end

#outputObject



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

def output
  @output
end

#pipeline_idObject



229
230
231
# File 'lib/hatchet/app.rb', line 229

def pipeline_id
  @pipeline_id
end

#platform_apiObject



255
256
257
# File 'lib/hatchet/app.rb', line 255

def platform_api
  @platform_api ||= PlatformAPI.connect_oauth(api_key)
end

#pushObject Also known as: push!, push_with_retry



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

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)


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

def push_without_retry!
  raise NotImplementedError
end

#retry_error_message(error, attempt, max_retries) ⇒ Object



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

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



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

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



207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/hatchet/app.rb', line 207

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: @buildpack,
                         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



47
48
49
50
51
# File 'lib/hatchet/app.rb', line 47

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

#set_lab(lab) ⇒ Object



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

def set_lab(lab)
  heroku.post_feature(lab, name)
end

#set_labs!Object



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

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

#setup!Object Also known as: setup

creates a new heroku app via the API



125
126
127
128
129
130
131
132
# File 'lib/hatchet/app.rb', line 125

def setup!
  return self if @app_is_setup
  puts "Hatchet setup: #{name.inspect} for #{repo_name.inspect}"
  create_app
  set_labs!
  @app_is_setup = true
  self
end

#source_get_urlObject



237
238
239
240
# File 'lib/hatchet/app.rb', line 237

def source_get_url
  create_source
  @source_get_url
end

#teardown!Object



139
140
141
142
143
144
145
146
# File 'lib/hatchet/app.rb', line 139

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