fly-rails 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +202 -0
- data/README.md +100 -0
- data/Rakefile +3 -0
- data/lib/fly-rails/actions.rb +652 -0
- data/lib/fly-rails/deploy.rb +42 -0
- data/lib/fly-rails/dsl.rb +92 -0
- data/lib/fly-rails/generators.rb +3 -0
- data/lib/fly-rails/hcl.rb +99 -0
- data/lib/fly-rails/machines.rb +209 -0
- data/lib/fly-rails/platforms.rb +10 -0
- data/lib/fly-rails/scanner.rb +68 -0
- data/lib/fly-rails/utils.rb +66 -0
- data/lib/fly-rails/version.rb +3 -0
- data/lib/fly-rails.rb +27 -0
- data/lib/generators/fly/app_generator.rb +72 -0
- data/lib/generators/fly/config_generator.rb +19 -0
- data/lib/generators/fly/terraform_generator.rb +36 -0
- data/lib/generators/templates/Dockerfile.erb +270 -0
- data/lib/generators/templates/Procfile.fly.erb +3 -0
- data/lib/generators/templates/dockerignore.erb +16 -0
- data/lib/generators/templates/fly.rake.erb +101 -0
- data/lib/generators/templates/fly.rb.erb +27 -0
- data/lib/generators/templates/fly.toml.erb +81 -0
- data/lib/generators/templates/hook_detached_process.erb +7 -0
- data/lib/generators/templates/litefs.yml.erb +14 -0
- data/lib/generators/templates/main.tf.erb +101 -0
- data/lib/generators/templates/nginx.conf.erb +77 -0
- data/lib/generators/templates/patches/action_cable.rb +20 -0
- data/lib/tasks/fly.rake +178 -0
- data/lib/tasks/mock.rake +17 -0
- metadata +87 -0
@@ -0,0 +1,652 @@
|
|
1
|
+
require 'open3'
|
2
|
+
require 'thor'
|
3
|
+
require 'toml'
|
4
|
+
require 'active_support'
|
5
|
+
require 'active_support/core_ext/string/inflections'
|
6
|
+
require 'fly-rails/machines'
|
7
|
+
require 'fly-rails/utils'
|
8
|
+
require 'fly-rails/dsl'
|
9
|
+
require 'fly-rails/scanner'
|
10
|
+
|
11
|
+
module Fly
|
12
|
+
class Actions < Thor::Group
|
13
|
+
include Thor::Actions
|
14
|
+
include Thor::Base
|
15
|
+
include Thor::Shell
|
16
|
+
include Fly::Scanner
|
17
|
+
attr_accessor :options, :dockerfile, :ignorefile
|
18
|
+
|
19
|
+
def initialize(app=nil, options={})
|
20
|
+
# placate thor
|
21
|
+
@options = {}
|
22
|
+
@destination_stack = [Dir.pwd]
|
23
|
+
|
24
|
+
# extract options
|
25
|
+
app ? self.app = app : app = self.app
|
26
|
+
regions = options[:region]&.flatten || []
|
27
|
+
@avahi = options[:avahi]
|
28
|
+
@litefs = options[:litefs]
|
29
|
+
@nats = options[:nats]
|
30
|
+
@nomad = options[:nomad]
|
31
|
+
@passenger = options[:passenger]
|
32
|
+
@serverless = options[:serverless]
|
33
|
+
@eject = options[:eject]
|
34
|
+
|
35
|
+
# prepare template variables
|
36
|
+
@ruby_version = RUBY_VERSION
|
37
|
+
@bundler_version = Bundler::VERSION
|
38
|
+
@node = File.exist? 'node_modules'
|
39
|
+
@yarn = File.exist? 'yarn.lock'
|
40
|
+
@node_version = @node ? `node --version`.chomp.sub(/^v/, '') : '16.17.0'
|
41
|
+
@yarn_version = @yarn ? `yarn --version`.chomp : 'latest'
|
42
|
+
@org = Fly::Machines.org
|
43
|
+
|
44
|
+
@set_stage = @nomad ? 'set' : 'set --stage'
|
45
|
+
|
46
|
+
# determine region
|
47
|
+
if !regions or regions.empty?
|
48
|
+
@regions = JSON.parse(`flyctl regions list --json --app #{app}`)['Regions'].
|
49
|
+
map {|region| region['Code']} rescue []
|
50
|
+
else
|
51
|
+
@regions = regions
|
52
|
+
end
|
53
|
+
|
54
|
+
@region = @regions.first || 'iad'
|
55
|
+
@regions = [@region] if @regions.empty?
|
56
|
+
|
57
|
+
# Process DSL
|
58
|
+
@config = Fly::DSL::Config.new
|
59
|
+
if File.exist? 'config/fly.rb'
|
60
|
+
@config.instance_eval IO.read('config/fly.rb')
|
61
|
+
@image = @config.image
|
62
|
+
end
|
63
|
+
|
64
|
+
# set additional variables based on application source
|
65
|
+
scan_rails_app
|
66
|
+
@redis = :internal if options[:redis]
|
67
|
+
if File.exist? 'Procfile.fly'
|
68
|
+
@redis = :internal if IO.read('Procfile.fly') =~ /^redis/
|
69
|
+
end
|
70
|
+
|
71
|
+
if options[:anycable] and not @anycable
|
72
|
+
# read and remove original config
|
73
|
+
original_config = YAML.load_file 'config/cable.yml'
|
74
|
+
File.unlink 'config/cable.yml'
|
75
|
+
|
76
|
+
# add and configure anycable-rails
|
77
|
+
say_status :run, 'bundle add anycable-rails'
|
78
|
+
Bundler.with_original_env do
|
79
|
+
system 'bundle add anycable-rails'
|
80
|
+
system 'bin/rails generate anycable:setup --skip-heroku --skip-procfile-dev --skip-jwt --devenv=skip'
|
81
|
+
end
|
82
|
+
|
83
|
+
# insert action_cable_meta_tag
|
84
|
+
insert_into_file 'app/views/layouts/application.html.erb',
|
85
|
+
" <%= action_cable_meta_tag %>\n",
|
86
|
+
after: "<%= csp_meta_tag %>\n"
|
87
|
+
|
88
|
+
# copy production environment to original config
|
89
|
+
anycable_config = YAML.load_file 'config/cable.yml'
|
90
|
+
original_config['production'] = anycable_config['production']
|
91
|
+
File.write 'config/cable.yml', YAML.dump(original_config)
|
92
|
+
|
93
|
+
@anycable = true
|
94
|
+
end
|
95
|
+
|
96
|
+
@nginx = @passenger || (@anycable and not @deploy)
|
97
|
+
|
98
|
+
# determine processes
|
99
|
+
@procs = {web: 'bin/rails server'}
|
100
|
+
@procs[:web] = "nginx -g 'daemon off;'" if @nginx
|
101
|
+
@procs[:rails] = "bin/rails server -p 8081" if @nginx and not @passenger
|
102
|
+
@procs[:worker] = 'bundle exec sidekiq' if @sidekiq
|
103
|
+
@procs[:redis] = 'redis-server /etc/redis/redis.conf' if @redis == :internal
|
104
|
+
@procs.merge! 'anycable-rpc': 'bundle exec anycable --rpc-host=0.0.0.0:50051',
|
105
|
+
'anycable-go': 'env /usr/local/bin/anycable-go --port=8082 --host 0.0.0.0 --rpc_host=localhost:50051' if @anycable
|
106
|
+
end
|
107
|
+
|
108
|
+
def app
|
109
|
+
return @app if @app
|
110
|
+
self.app = TOML.load_file('fly.toml')['app']
|
111
|
+
end
|
112
|
+
|
113
|
+
def render template
|
114
|
+
template = ERB.new(IO.read(File.expand_path(template, source_paths.last)), trim_mode: '-')
|
115
|
+
template.result(binding).chomp
|
116
|
+
end
|
117
|
+
|
118
|
+
def app_template template_file, destination
|
119
|
+
app
|
120
|
+
template template_file, destination
|
121
|
+
end
|
122
|
+
|
123
|
+
def app=(app)
|
124
|
+
@app = app
|
125
|
+
@appName = @app.gsub('-', '_').camelcase(:lower)
|
126
|
+
end
|
127
|
+
|
128
|
+
source_paths.push File::expand_path('../generators/templates', __dir__)
|
129
|
+
|
130
|
+
def generate_toml
|
131
|
+
app_template 'fly.toml.erb', 'fly.toml'
|
132
|
+
end
|
133
|
+
|
134
|
+
def generate_fly_config
|
135
|
+
select_image
|
136
|
+
app_template 'fly.rb.erb', 'config/fly.rb'
|
137
|
+
end
|
138
|
+
|
139
|
+
def select_image
|
140
|
+
return @image if @image and @image.include? ":#{@ruby_version}-"
|
141
|
+
|
142
|
+
tags = []
|
143
|
+
|
144
|
+
debian_releases = %w(stretch buster bullseye bookworm)
|
145
|
+
|
146
|
+
Net::HTTP.start('quay.io', 443, use_ssl: true) do |http|
|
147
|
+
(1..).each do |page|
|
148
|
+
request = Net::HTTP::Get.new "/api/v1/repository/evl.ms/fullstaq-ruby/tag/?page=#{page}&limit=100"
|
149
|
+
response = http.request request
|
150
|
+
body = JSON.parse(response.body)
|
151
|
+
tags += body['tags'].map {|tag| tag['name']}.grep /jemalloc-\w+-slim/
|
152
|
+
break unless body['has_additional']
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
ruby_releases = tags.group_by {|tag| tag.split('-').first}.
|
157
|
+
map do |release, tags|
|
158
|
+
[release, tags.max_by {|tag| debian_releases.find_index(tag[/jemalloc-(\w+)-slim/, 1]) || -1}]
|
159
|
+
end.sort.to_h
|
160
|
+
|
161
|
+
unless ruby_releases[@ruby_version]
|
162
|
+
@ruby_version = ruby_releases.keys.find {|release| release >= @ruby_version} ||
|
163
|
+
ruby_releases.keys.last
|
164
|
+
end
|
165
|
+
|
166
|
+
@image = 'quay.io/evl.ms/fullstaq-ruby:' + ruby_releases[@ruby_version]
|
167
|
+
end
|
168
|
+
|
169
|
+
def generate_dockerfile
|
170
|
+
if @eject or File.exist? 'Dockerfile'
|
171
|
+
@dockerfile = 'Dockerfile'
|
172
|
+
else
|
173
|
+
tmpfile = Tempfile.new('Dockerfile')
|
174
|
+
@dockerfile = tmpfile.path
|
175
|
+
tmpfile.unlink
|
176
|
+
at_exit { File.unlink @dockerfile }
|
177
|
+
end
|
178
|
+
|
179
|
+
if @eject or not File.exist? @dockerfile
|
180
|
+
select_image
|
181
|
+
app_template 'Dockerfile.erb', @dockerfile
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def generate_dockerignore
|
186
|
+
if @eject or File.exist? '.dockerignore'
|
187
|
+
@ignorefile = '.dockerignore'
|
188
|
+
elsif File.exist? '.gitignore'
|
189
|
+
@ignorefile = '.gitignore'
|
190
|
+
else
|
191
|
+
tmpfile = Tempfile.new('Dockerignore')
|
192
|
+
@ignoreile = tmpfile.path
|
193
|
+
tmpfile.unlink
|
194
|
+
at_exit { Filee.unlink @ignorefile }
|
195
|
+
end
|
196
|
+
|
197
|
+
if @eject or not File.exist? @ignorefile
|
198
|
+
app_template 'dockerignore.erb', @ignorefile
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def generate_nginx_conf
|
203
|
+
return unless @passenger
|
204
|
+
app_template 'nginx.conf.erb', 'config/nginx.conf'
|
205
|
+
|
206
|
+
if @serverless
|
207
|
+
app_template 'hook_detached_process.erb', 'config/hook_detached_process'
|
208
|
+
FileUtils.chmod 'u+x', 'config/hook_detached_process'
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def generate_terraform
|
213
|
+
app_template 'main.tf.erb', 'main.tf'
|
214
|
+
end
|
215
|
+
|
216
|
+
def generate_raketask
|
217
|
+
app_template 'fly.rake.erb', 'lib/tasks/fly.rake'
|
218
|
+
end
|
219
|
+
|
220
|
+
def generate_procfile
|
221
|
+
return unless @procs.length > 1
|
222
|
+
app_template 'Procfile.fly.erb', 'Procfile.fly'
|
223
|
+
end
|
224
|
+
|
225
|
+
def generate_litefs
|
226
|
+
app_template 'litefs.yml.erb', 'config/litefs.yml'
|
227
|
+
end
|
228
|
+
|
229
|
+
def generate_key
|
230
|
+
credentials = nil
|
231
|
+
if File.exist? 'config/credentials/production.key'
|
232
|
+
credentials = 'config/credentials/production.key'
|
233
|
+
elsif File.exist? 'config/master.key'
|
234
|
+
credentials = 'config/master.key'
|
235
|
+
end
|
236
|
+
|
237
|
+
if credentials
|
238
|
+
say_status :run, "flyctl secrets #{@set_stage} RAILS_MASTER_KEY from #{credentials}"
|
239
|
+
system "flyctl secrets #{@set_stage} RAILS_MASTER_KEY=#{IO.read(credentials).chomp}"
|
240
|
+
puts
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
def generate_patches
|
245
|
+
if false # @redis_cable and not @anycable and @redis != :internal and
|
246
|
+
not File.exist? 'config/initializers/action_cable.rb'
|
247
|
+
|
248
|
+
app
|
249
|
+
template 'patches/action_cable.rb', 'config/initializers/action_cable.rb'
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
def generate_ipv4
|
254
|
+
cmd = 'flyctl ips allocate-v4'
|
255
|
+
say_status :run, cmd
|
256
|
+
system cmd
|
257
|
+
end
|
258
|
+
|
259
|
+
def generate_ipv6
|
260
|
+
cmd = 'flyctl ips allocate-v6'
|
261
|
+
say_status :run, cmd
|
262
|
+
system cmd
|
263
|
+
end
|
264
|
+
|
265
|
+
def create_volume(app, region, size)
|
266
|
+
name = "#{app.gsub('-', '_')}_volume"
|
267
|
+
volumes = JSON.parse(`flyctl volumes list --json`)
|
268
|
+
|
269
|
+
volume = volumes.find {|volume| volume['Name'] == name and volume['Region'] == region}
|
270
|
+
unless volume
|
271
|
+
cmd = "flyctl volumes create #{name} --app #{app} --region #{region} --size #{size}"
|
272
|
+
say_status :run, cmd
|
273
|
+
system cmd
|
274
|
+
volumes = JSON.parse(`flyctl volumes list --json`)
|
275
|
+
volume = volumes.find {|volume| volume['Name'] == name and volume['Region'] == region}
|
276
|
+
end
|
277
|
+
|
278
|
+
volume && volume['id']
|
279
|
+
end
|
280
|
+
|
281
|
+
def create_postgres(app, org, region, vm_size, volume_size, cluster_size)
|
282
|
+
cmd = "flyctl postgres create --name #{app}-db --org #{org} --region #{region} --vm-size #{vm_size} --volume-size #{volume_size} --initial-cluster-size #{cluster_size}"
|
283
|
+
cmd += ' --machines' unless @nomad
|
284
|
+
say_status :run, cmd
|
285
|
+
output = FlyIoRails::Utils.tee(cmd)
|
286
|
+
output[%r{postgres://\S+}]
|
287
|
+
end
|
288
|
+
|
289
|
+
def create_redis(app, org, region, eviction)
|
290
|
+
# see if redis is already defined
|
291
|
+
name = `flyctl redis list`.lines[1..-2].map(&:split).
|
292
|
+
find {|tokens| tokens[1] == org}&.first
|
293
|
+
|
294
|
+
if name
|
295
|
+
secret = `flyctl redis status #{name}`[%r{redis://\S+}]
|
296
|
+
return secret if secret
|
297
|
+
end
|
298
|
+
|
299
|
+
# create a new redis
|
300
|
+
cmd = "flyctl redis create --org #{org} --name #{app}-redis --region #{region} --no-replicas #{eviction} --plan #{@config.redis.plan}"
|
301
|
+
say_status :run, cmd
|
302
|
+
output = FlyIoRails::Utils.tee(cmd)
|
303
|
+
output[%r{redis://[-\w:@.]+}]
|
304
|
+
end
|
305
|
+
|
306
|
+
def bundle_gems
|
307
|
+
if @anycable and not @gemfile.include? 'anycable-rails'
|
308
|
+
cmd = 'bundle add anycable-rails'
|
309
|
+
say_status :run, cmd
|
310
|
+
Bundler.with_original_env { system cmd }
|
311
|
+
exit $?.exitstatus unless $?.success?
|
312
|
+
end
|
313
|
+
|
314
|
+
if @postgresql and not @gemfile.include? 'pg'
|
315
|
+
cmd = 'bundle add pg'
|
316
|
+
say_status :run, cmd
|
317
|
+
Bundler.with_original_env { system cmd }
|
318
|
+
exit $?.exitstatus unless $?.success?
|
319
|
+
end
|
320
|
+
|
321
|
+
if @redis and not @gemfile.include? 'redis'
|
322
|
+
cmd = 'bundle add redis'
|
323
|
+
say_status :run, cmd
|
324
|
+
Bundler.with_original_env { system cmd }
|
325
|
+
exit $?.exitstatus unless $?.success?
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
def release(app, options)
|
330
|
+
start = Fly::Machines.create_and_start_machine(app, options)
|
331
|
+
machine = start[:id]
|
332
|
+
|
333
|
+
if not machine
|
334
|
+
STDERR.puts 'Error starting release machine'
|
335
|
+
PP.pp start, STDERR
|
336
|
+
exit 1
|
337
|
+
end
|
338
|
+
|
339
|
+
status = Fly::Machines.wait_for_machine app, machine,
|
340
|
+
timeout: 60, state: 'started'
|
341
|
+
|
342
|
+
# wait for release to copmlete
|
343
|
+
5.times do
|
344
|
+
status = Fly::Machines.wait_for_machine app, machine,
|
345
|
+
instance_id: start[:instance_id], timeout: 60, state: 'stopped'
|
346
|
+
break if status[:ok]
|
347
|
+
end
|
348
|
+
|
349
|
+
if status and status[:ok]
|
350
|
+
event = nil
|
351
|
+
300.times do
|
352
|
+
status = Fly::Machines.get_a_machine app, start[:id]
|
353
|
+
event = status[:events]&.first
|
354
|
+
break if event[:type] == 'exit'
|
355
|
+
sleep 0.2
|
356
|
+
end
|
357
|
+
|
358
|
+
exit_code = event&.dig(:request, :MonitorEvent, :exit_event, :exit_code)
|
359
|
+
Fly::Machines.delete_machine app, machine if machine
|
360
|
+
exit_code ||= 0 if event&.dig(:request, :MonitorEvent, :exit_event, :signal) == -1
|
361
|
+
return event, exit_code, machine
|
362
|
+
else
|
363
|
+
return status, nil, nil
|
364
|
+
end
|
365
|
+
end
|
366
|
+
|
367
|
+
def launch(app)
|
368
|
+
secrets = JSON.parse(`flyctl secrets list --json`).
|
369
|
+
map {|secret| secret["Name"]}
|
370
|
+
|
371
|
+
unless secrets.include? 'RAILS_MASTER_KEY'
|
372
|
+
generate_key
|
373
|
+
end
|
374
|
+
|
375
|
+
if @sqlite3
|
376
|
+
if @litefs
|
377
|
+
@regions.each do |region|
|
378
|
+
@volume = create_volume(app, region, @config.sqlite3.size)
|
379
|
+
end
|
380
|
+
else
|
381
|
+
@volume = create_volume(app, @region, @config.sqlite3.size)
|
382
|
+
end
|
383
|
+
elsif @postgresql and not secrets.include? 'DATABASE_URL'
|
384
|
+
unless (IO.read('config/fly.rb').include?('postgres') rescue true)
|
385
|
+
source_paths.each do |path|
|
386
|
+
template = File.join(path, 'fly.rb.erb')
|
387
|
+
next unless File.exist? template
|
388
|
+
insert = IO.read(template)[/<% if @postgresql -%>\n(.*?)<% end/m, 1]
|
389
|
+
append_to_file 'config/fly.rb', insert if insert
|
390
|
+
break
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
secret = create_postgres(app, @org, @region,
|
395
|
+
@config.postgres.vm_size,
|
396
|
+
@config.postgres.volume_size,
|
397
|
+
@config.postgres.initial_cluster_size)
|
398
|
+
|
399
|
+
if secret
|
400
|
+
cmd = "flyctl secrets #{@set_stage} DATABASE_URL=#{secret}"
|
401
|
+
say_status :run, cmd
|
402
|
+
system cmd
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
if @redis and @redis != :internal and not secrets.include? 'REDIS_URL'
|
407
|
+
# Set eviction policy to true if a cache provider, else false.
|
408
|
+
eviction = @redis_cache ? '--enable-eviction' : '--disable-eviction'
|
409
|
+
|
410
|
+
secret = create_redis(app, @org, @region, eviction)
|
411
|
+
|
412
|
+
if secret
|
413
|
+
cmd = "flyctl secrets #{@set_stage} REDIS_URL=#{secret}"
|
414
|
+
say_status :run, cmd
|
415
|
+
system cmd
|
416
|
+
end
|
417
|
+
end
|
418
|
+
end
|
419
|
+
|
420
|
+
def release_task_defined?
|
421
|
+
if File.exist? 'lib/tasks/fly.rake'
|
422
|
+
Rake.load_rakefile 'lib/tasks/fly.rake'
|
423
|
+
else
|
424
|
+
Tempfile.create ['fly', '.rake'] do |file|
|
425
|
+
IO.write file.path, render('fly.rake.erb')
|
426
|
+
Rake.load_rakefile file.path
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
if Rake::Task.task_defined? 'fly:release'
|
431
|
+
task = Rake::Task['fly:release']
|
432
|
+
not (task.actions.empty? and task.prereqs.empty?)
|
433
|
+
else
|
434
|
+
false
|
435
|
+
end
|
436
|
+
end
|
437
|
+
|
438
|
+
def deploy(app, image)
|
439
|
+
launch(app)
|
440
|
+
|
441
|
+
# default config
|
442
|
+
config = {
|
443
|
+
image: image,
|
444
|
+
guest: {
|
445
|
+
cpus: @config.machine.cpus,
|
446
|
+
cpu_kind: @config.machine.cpu_kind,
|
447
|
+
memory_mb: @config.machine.memory_mb
|
448
|
+
},
|
449
|
+
services: [
|
450
|
+
{
|
451
|
+
ports: [
|
452
|
+
{port: 443, handlers: ["tls", "http"]},
|
453
|
+
{port: 80, handlers: ["http"]}
|
454
|
+
],
|
455
|
+
protocol: "tcp",
|
456
|
+
internal_port: 8080
|
457
|
+
}
|
458
|
+
]
|
459
|
+
}
|
460
|
+
|
461
|
+
# start proxy, if necessary
|
462
|
+
Fly::Machines::fly_api_hostname!
|
463
|
+
|
464
|
+
# only run release step if there is a non-empty release task in fly.rake
|
465
|
+
if release_task_defined?
|
466
|
+
# build config for release machine, overriding server command
|
467
|
+
release_config = config.dup
|
468
|
+
release_config.delete :services
|
469
|
+
release_config.delete :mounts
|
470
|
+
release_config[:processes] = [{
|
471
|
+
name: 'release',
|
472
|
+
entrypoint: [],
|
473
|
+
cmd: ['bin/rails', 'fly:release'],
|
474
|
+
env: {},
|
475
|
+
user: 'root'
|
476
|
+
}]
|
477
|
+
|
478
|
+
# perform release
|
479
|
+
say_status :fly, 'bin/rails fly:release'
|
480
|
+
event, exit_code, machine = release(app, region: @region, config: release_config)
|
481
|
+
|
482
|
+
if exit_code != 0
|
483
|
+
STDERR.puts 'Error performing release'
|
484
|
+
STDERR.puts (exit_code ? {exit_code: exit_code} : event).inspect
|
485
|
+
STDERR.puts "run 'flyctl logs --instance #{machine}' for more information"
|
486
|
+
exit 1
|
487
|
+
end
|
488
|
+
end
|
489
|
+
|
490
|
+
# stop previous instances - list will fail on first run
|
491
|
+
stdout, stderr, status = Open3.capture3('fly machines list --json')
|
492
|
+
existing_machines = []
|
493
|
+
unless stdout.empty?
|
494
|
+
JSON.parse(stdout).each do |list|
|
495
|
+
existing_machines << list['name']
|
496
|
+
next if list['id'] == machine or list['state'] == 'destroyed'
|
497
|
+
cmd = "fly machines remove --force #{list['id']}"
|
498
|
+
say_status :run, cmd
|
499
|
+
system cmd
|
500
|
+
end
|
501
|
+
end
|
502
|
+
|
503
|
+
# configure sqlite3 (can be overridden by fly.toml)
|
504
|
+
if @sqlite3
|
505
|
+
config[:mounts] = [
|
506
|
+
{ volume: @volume, path: '/mnt/volume' }
|
507
|
+
]
|
508
|
+
|
509
|
+
config[:env] = {
|
510
|
+
"DATABASE_URL" => "sqlite3:///mnt/volume/production.sqlite3"
|
511
|
+
}
|
512
|
+
|
513
|
+
if @litefs
|
514
|
+
config[:env]['DATABASE_URL'] = "sqlite3:///data/production.sqlite3"
|
515
|
+
end
|
516
|
+
end
|
517
|
+
|
518
|
+
# process toml overrides
|
519
|
+
toml = (TOML.load_file('fly.toml') rescue {})
|
520
|
+
config[:env] = toml['env'] if toml['env']
|
521
|
+
config[:services] = toml['services'] if toml['services']
|
522
|
+
if toml['mounts']
|
523
|
+
mounts = toml['mounts']
|
524
|
+
volume = JSON.parse(`flyctl volumes list --json`).
|
525
|
+
find {|volume| volume['Name'] == mounts['source'] and volume['Region'] == @region}
|
526
|
+
if volume
|
527
|
+
config[:mounts] = [ { volume: volume['id'], path: mounts['destination'] } ]
|
528
|
+
else
|
529
|
+
STDERR.puts "volume #{mounts['source']} not found in region #{@region}"
|
530
|
+
exit 1
|
531
|
+
end
|
532
|
+
end
|
533
|
+
|
534
|
+
# start app
|
535
|
+
machines = {}
|
536
|
+
options = {region: @region, config: config}
|
537
|
+
say_status :fly, "start #{app}"
|
538
|
+
if not toml['processes'] or toml['processes'].empty?
|
539
|
+
options[:name] = "#{app}-machine"
|
540
|
+
taken = existing_machines.find {|name| name.start_with? options[:name]}
|
541
|
+
options[:name] = taken == options[:name] ? "#{taken}-2" : taken.next if taken
|
542
|
+
|
543
|
+
start = Fly::Machines.create_and_start_machine(app, options)
|
544
|
+
machines['app'] = start[:id]
|
545
|
+
else
|
546
|
+
config[:env] ||= {}
|
547
|
+
config[:env]['NATS_SERVER'] = 'localhost'
|
548
|
+
toml['processes'].each do |name, entrypoint|
|
549
|
+
options[:name] = "#{app}-machine-#{name}"
|
550
|
+
taken = existing_machines.find {|name| name.start_with? options[:name]}
|
551
|
+
options[:name] = taken == options[:name] ? "#{taken}-2" : taken.next if taken
|
552
|
+
|
553
|
+
config[:env]['SERVER_COMMAND'] = entrypoint
|
554
|
+
start = Fly::Machines.create_and_start_machine(app, options)
|
555
|
+
|
556
|
+
if start['error']
|
557
|
+
STDERR.puts "ERROR: #{start['error']}"
|
558
|
+
exit 1
|
559
|
+
end
|
560
|
+
|
561
|
+
machines[name] = start[:id]
|
562
|
+
|
563
|
+
config.delete :mounts
|
564
|
+
config.delete :services
|
565
|
+
|
566
|
+
if config[:env]['NATS_SERVER'] = 'localhost'
|
567
|
+
config[:env]['NATS_SERVER'] = start[:private_ip]
|
568
|
+
end
|
569
|
+
end
|
570
|
+
end
|
571
|
+
|
572
|
+
if machines.empty?
|
573
|
+
STDERR.puts 'Error starting application'
|
574
|
+
PP.pp start, STDERR
|
575
|
+
exit 1
|
576
|
+
end
|
577
|
+
|
578
|
+
timeout = Time.now + 300
|
579
|
+
while Time.now < timeout and not machines.empty?
|
580
|
+
machines.each do |name, machine|
|
581
|
+
status = Fly::Machines.wait_for_machine app, machine,
|
582
|
+
timeout: 10, status: 'started'
|
583
|
+
machines.delete name if status[:ok]
|
584
|
+
end
|
585
|
+
end
|
586
|
+
|
587
|
+
unless machines.empty?
|
588
|
+
STDERR.puts 'Timeout waiting for application to start'
|
589
|
+
end
|
590
|
+
end
|
591
|
+
|
592
|
+
def terraform(app, image)
|
593
|
+
# find first machine using the image ref in terraform config file
|
594
|
+
machine = Fly::HCL.parse(IO.read('main.tf')).
|
595
|
+
map {|block| block.dig(:resource, 'fly_machine')}.compact.
|
596
|
+
find {|machine| machine.values.first[:image] == 'var.image_ref'}
|
597
|
+
if not machine
|
598
|
+
STDERR.puts 'unable to find fly_machine with image = var.image_ref in main.rf'
|
599
|
+
exit 1
|
600
|
+
end
|
601
|
+
|
602
|
+
# extract HCL configuration for the machine
|
603
|
+
config = machine.values.first
|
604
|
+
|
605
|
+
# delete HCL specific configuration items
|
606
|
+
%i(services for_each region app name depends_on).each do |key|
|
607
|
+
config.delete key
|
608
|
+
end
|
609
|
+
|
610
|
+
# move machine configuration into guest object
|
611
|
+
config[:guest] = {
|
612
|
+
cpus: config.delete(:cpus),
|
613
|
+
memory_mb: config.delete(:memorymb),
|
614
|
+
cpu_kind: config.delete(:cputype)
|
615
|
+
}
|
616
|
+
|
617
|
+
# release machines should have no services or mounts
|
618
|
+
config.delete :services
|
619
|
+
config.delete :mounts
|
620
|
+
|
621
|
+
# override start command
|
622
|
+
config[:env] ||= {}
|
623
|
+
config[:env]['SERVER_COMMAND'] = 'bin/rails fly:release'
|
624
|
+
|
625
|
+
# fill in image
|
626
|
+
config[:image] = image
|
627
|
+
|
628
|
+
# start proxy, if necessary
|
629
|
+
endpoint = Fly::Machines::fly_api_hostname!
|
630
|
+
|
631
|
+
# perform release, if necessary
|
632
|
+
if release_task_defined?
|
633
|
+
say_status :fly, config[:env]['SERVER_COMMAND']
|
634
|
+
event, exit_code, machine = release(app, region: @region, config: config)
|
635
|
+
else
|
636
|
+
exit_code = 0
|
637
|
+
end
|
638
|
+
|
639
|
+
if exit_code == 0
|
640
|
+
# use terraform apply to deploy
|
641
|
+
ENV['FLY_API_TOKEN'] = `flyctl auth token`.chomp
|
642
|
+
ENV['FLY_HTTP_ENDPOINT'] = endpoint if endpoint
|
643
|
+
system "terraform apply -auto-approve -var=\"image_ref=#{image}\""
|
644
|
+
else
|
645
|
+
STDERR.puts 'Error performing release'
|
646
|
+
STDERR.puts (exit_code ? {exit_code: exit_code} : event).inspect
|
647
|
+
STDERR.puts "run 'flyctl logs --instance #{machine}' for more information"
|
648
|
+
exit 1
|
649
|
+
end
|
650
|
+
end
|
651
|
+
end
|
652
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
$:.unshift File.expand_path('lib')
|
2
|
+
require 'fly-rails/actions'
|
3
|
+
|
4
|
+
require 'bundler'
|
5
|
+
require 'pp'
|
6
|
+
|
7
|
+
def check_git
|
8
|
+
return if Dir.exist? '/srv/fly-rails/lib'
|
9
|
+
|
10
|
+
spec = Bundler::Definition.build('Gemfile', nil, []).dependencies.
|
11
|
+
find {|spec| spec.name == 'fly-rails'}
|
12
|
+
|
13
|
+
if spec.git
|
14
|
+
if `which git`.empty? and File.exist? '/etc/debian_version'
|
15
|
+
system 'apt-get update'
|
16
|
+
system 'apt-get install -y git'
|
17
|
+
end
|
18
|
+
|
19
|
+
system `git clone --depth 1 #{spec.git} /srv/fly-rails`
|
20
|
+
exit 1 unless Dir.exist? '/srv/fly-rails/lib'
|
21
|
+
ENV['RUBYLIB'] = '/srv/fly-rails/lib'
|
22
|
+
exec "ruby -r fly-rails/deploy -e #{caller_locations(1,1)[0].label}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def dump_config
|
27
|
+
action = Fly::Actions.new
|
28
|
+
|
29
|
+
config = {}
|
30
|
+
action.instance_variables.sort.each do |name|
|
31
|
+
config[name] = action.instance_variable_get(name)
|
32
|
+
end
|
33
|
+
|
34
|
+
File.open('/srv/config', 'w') {|file| PP.pp(config, file)}
|
35
|
+
end
|
36
|
+
|
37
|
+
def build_gems
|
38
|
+
check_git
|
39
|
+
dump_config
|
40
|
+
|
41
|
+
system 'rake -f lib/tasks/fly.rake fly:build_gems'
|
42
|
+
end
|