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,92 @@
|
|
1
|
+
require 'rake'
|
2
|
+
|
3
|
+
module Fly
|
4
|
+
module DSL
|
5
|
+
class Base
|
6
|
+
include Rake::DSL
|
7
|
+
|
8
|
+
@@blocks = {}
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@value = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.block name, kind
|
15
|
+
@@blocks[name] = kind
|
16
|
+
|
17
|
+
define_method name do |&block|
|
18
|
+
@config[name] ||= kind.new
|
19
|
+
@config[name].instance_eval(&block) if block
|
20
|
+
@config[name]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.blocks
|
25
|
+
@@blocks
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.option name, default=nil
|
29
|
+
@options ||= {}
|
30
|
+
@options[name] = default
|
31
|
+
|
32
|
+
define_method name do |*args|
|
33
|
+
if args.length == 1
|
34
|
+
@value[name] = args.first
|
35
|
+
elsif args.length > 1
|
36
|
+
raise ArgumentError.new("wrong number of arguments (given #{args.length}, expected 0..1)")
|
37
|
+
end
|
38
|
+
|
39
|
+
@value.include?(name) ? @value[name] : default
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.options
|
44
|
+
@options ||= {}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
#############################################################
|
49
|
+
|
50
|
+
class Machine < Base
|
51
|
+
option :cpus, 1
|
52
|
+
option :cpu_kind, 'shared'
|
53
|
+
option :memory_mb, 256
|
54
|
+
end
|
55
|
+
|
56
|
+
class Postgres < Base
|
57
|
+
option :vm_size, 'shared-cpu-1x'
|
58
|
+
option :volume_size, 1
|
59
|
+
option :initial_cluster_size, 1
|
60
|
+
end
|
61
|
+
|
62
|
+
class Redis < Base
|
63
|
+
option :plan, "Free"
|
64
|
+
end
|
65
|
+
|
66
|
+
class Sqlite3 < Base
|
67
|
+
option :size, 3
|
68
|
+
end
|
69
|
+
|
70
|
+
class Deploy < Base
|
71
|
+
option :swap_mb, 0
|
72
|
+
end
|
73
|
+
|
74
|
+
#############################################################
|
75
|
+
|
76
|
+
class Config < Base
|
77
|
+
|
78
|
+
def initialize
|
79
|
+
@config = {}
|
80
|
+
super
|
81
|
+
end
|
82
|
+
|
83
|
+
option :image, nil
|
84
|
+
|
85
|
+
block :machine, Machine
|
86
|
+
block :postgres, Postgres
|
87
|
+
block :redis, Redis
|
88
|
+
block :sqlite3, Sqlite3
|
89
|
+
block :deploy, Deploy
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'strscan'
|
2
|
+
|
3
|
+
module Fly
|
4
|
+
# a very liberal HCL scanner
|
5
|
+
module HCL
|
6
|
+
def self.parse(string)
|
7
|
+
result = []
|
8
|
+
name = nil
|
9
|
+
stack = []
|
10
|
+
block = {}
|
11
|
+
cursor = block
|
12
|
+
result.push block
|
13
|
+
|
14
|
+
hcl = StringScanner.new(string)
|
15
|
+
until hcl.eos?
|
16
|
+
hcl.scan(%r{\s*(\#.*|//.*|/\*[\S\s]*?\*/)*})
|
17
|
+
|
18
|
+
if hcl.scan(/[a-zA-Z]\S*|\d[.\d]*|"[^"]*"/)
|
19
|
+
if cursor.is_a? Array
|
20
|
+
cursor.push token(hcl.matched)
|
21
|
+
elsif name == nil
|
22
|
+
name = token(hcl.matched)
|
23
|
+
else
|
24
|
+
hash = {}
|
25
|
+
cursor[name] = hash
|
26
|
+
name = token(hcl.matched)
|
27
|
+
cursor = hash
|
28
|
+
end
|
29
|
+
elsif hcl.scan(/=/)
|
30
|
+
hcl.scan(/\s*/)
|
31
|
+
if hcl.scan(/\[/)
|
32
|
+
list = []
|
33
|
+
cursor[name] = list
|
34
|
+
name = nil
|
35
|
+
stack.push cursor
|
36
|
+
cursor = list
|
37
|
+
elsif hcl.scan(/\{/)
|
38
|
+
hash = {}
|
39
|
+
cursor[name] = hash
|
40
|
+
name = nil
|
41
|
+
stack.push cursor
|
42
|
+
cursor = hash
|
43
|
+
elsif hcl.scan(/.*/)
|
44
|
+
cursor[name] = token(hcl.matched)
|
45
|
+
name = nil
|
46
|
+
end
|
47
|
+
elsif hcl.scan(/\{/)
|
48
|
+
hash = {}
|
49
|
+
if cursor.is_a? Array
|
50
|
+
cursor << hash
|
51
|
+
else
|
52
|
+
cursor[name] = hash
|
53
|
+
end
|
54
|
+
name = nil
|
55
|
+
stack.push cursor
|
56
|
+
cursor = hash
|
57
|
+
elsif hcl.scan(/\[/)
|
58
|
+
list = []
|
59
|
+
stack.push cursor
|
60
|
+
cursor = list
|
61
|
+
elsif hcl.scan(/\}|\]/)
|
62
|
+
cursor = stack.pop
|
63
|
+
|
64
|
+
if stack.empty?
|
65
|
+
block = {}
|
66
|
+
cursor = block
|
67
|
+
result.push block
|
68
|
+
end
|
69
|
+
elsif hcl.scan(/[,=:]/)
|
70
|
+
nil
|
71
|
+
elsif hcl.scan(/.*/)
|
72
|
+
unless hcl.matched.empty?
|
73
|
+
STDERR.puts "unexpected input: #{hcl.matched.inspect}"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
result.pop if result.last.empty?
|
79
|
+
result
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
def self.token(match)
|
84
|
+
if match =~ /^\d/
|
85
|
+
if match =~ /^\d+$/
|
86
|
+
match.to_i
|
87
|
+
else
|
88
|
+
match.to_f
|
89
|
+
end
|
90
|
+
elsif match =~ /^\w+$/
|
91
|
+
match.to_sym
|
92
|
+
elsif match =~ /^"(.*)"$/
|
93
|
+
$1
|
94
|
+
else
|
95
|
+
match
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,209 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Fly
|
5
|
+
# Thin wrapper over https://fly.io/docs/reference/machines/
|
6
|
+
#
|
7
|
+
# *** WARNING ***
|
8
|
+
#
|
9
|
+
# No validation or escaping is done by this code. It is assuming that
|
10
|
+
# the caller is trusted and does pass through unsanitized user input.
|
11
|
+
#
|
12
|
+
module Machines
|
13
|
+
@@api_token = ENV['FLY_API_TOKEN']
|
14
|
+
@@fly_api_hostname = nil
|
15
|
+
|
16
|
+
# determine fly api hostname. Returns nil if no proxy is running
|
17
|
+
def self.fly_api_hostname
|
18
|
+
return @@fly_api_hostname if @@fly_api_hostname
|
19
|
+
|
20
|
+
Net::HTTP.get URI('http://_api.internal:4280')
|
21
|
+
@@fly_api_hostname = '_api.internal:4280'
|
22
|
+
rescue
|
23
|
+
begin
|
24
|
+
Net::HTTP.get URI('http://127.0.0.1:4280')
|
25
|
+
@@fly_api_hostname = '127.0.0.1:4280'
|
26
|
+
rescue
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# determine application's organization
|
32
|
+
def self.org
|
33
|
+
org = 'personal'
|
34
|
+
|
35
|
+
if File.exist? 'fly.toml'
|
36
|
+
require 'toml'
|
37
|
+
app = TOML.load_file('fly.toml')['app']
|
38
|
+
|
39
|
+
apps = JSON.parse(`flyctl list apps --json`) rescue []
|
40
|
+
|
41
|
+
apps.each do |info|
|
42
|
+
org = info['Organization'] if info['ID'] == app
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
org
|
47
|
+
end
|
48
|
+
|
49
|
+
# determine fly api hostname. Starts proxy if necessary
|
50
|
+
def self.fly_api_hostname!
|
51
|
+
hostname = fly_api_hostname
|
52
|
+
return hostname if hostname
|
53
|
+
|
54
|
+
pid = fork { exec "flyctl machines api-proxy --org #{org}" }
|
55
|
+
at_exit { Process.kill "INT", pid }
|
56
|
+
|
57
|
+
# wait up to 12.7 seconds for the proxy to start
|
58
|
+
wait = 0.1
|
59
|
+
6.times do
|
60
|
+
sleep wait
|
61
|
+
begin
|
62
|
+
Net::HTTP.get URI('http://127.0.0.1:4280')
|
63
|
+
@@fly_api_hostname = '127.0.0.1:4280'
|
64
|
+
break
|
65
|
+
rescue
|
66
|
+
wait *= 2
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
@@fly_api_hostname
|
71
|
+
end
|
72
|
+
|
73
|
+
# create_fly_application app_name: 'user-functions', org_slug: 'personal'
|
74
|
+
def self.create_fly_application options
|
75
|
+
post '/v1/apps', options
|
76
|
+
end
|
77
|
+
|
78
|
+
# get_application_details 'user-functions'
|
79
|
+
def self.get_application_details app
|
80
|
+
get "/v1/apps/#{app}"
|
81
|
+
end
|
82
|
+
|
83
|
+
# create_and_start_machine 'user-functions', name: 'quirky_machine', config: {
|
84
|
+
# image: 'flyio/fastify-functions',
|
85
|
+
# env: {'APP_ENV' => 'production'},
|
86
|
+
# services: [
|
87
|
+
# {
|
88
|
+
# ports: [
|
89
|
+
# {port: 443, handlers: ['tls', 'http']},
|
90
|
+
# {port: 80, handlers: ['http']}
|
91
|
+
# ],
|
92
|
+
# protocol: 'tcp',
|
93
|
+
# internal_protocol: 'tcp',
|
94
|
+
# }
|
95
|
+
# ]
|
96
|
+
# }
|
97
|
+
def self.create_and_start_machine app, options
|
98
|
+
post "/v1/apps/#{app}/machines", options
|
99
|
+
end
|
100
|
+
|
101
|
+
# wait_for_machine 'user-functions', '73d8d46dbee589'
|
102
|
+
def self.wait_for_machine app, machine, options = {timeout:60, status: 'started'}
|
103
|
+
get "/v1/apps/#{app}/machines/#{machine}/wait?#{options.to_query}"
|
104
|
+
end
|
105
|
+
|
106
|
+
# get_a_machine machine 'user-functions', '73d8d46dbee589'
|
107
|
+
def self.get_a_machine app, machine
|
108
|
+
get "/v1/apps/#{app}/machines/#{machine}"
|
109
|
+
end
|
110
|
+
|
111
|
+
# update_a_machine 'user-functions', '73d8d46dbee589', config: {
|
112
|
+
# image: 'flyio/fastify-functions',
|
113
|
+
# guest: { memory_mb: 512, cpus: 2 }
|
114
|
+
# }
|
115
|
+
def self.update_a_machine app, machine, options
|
116
|
+
post "/v1/apps/#{app}/machines/#{machine}", options
|
117
|
+
end
|
118
|
+
|
119
|
+
# stop_machine machine 'user-functions', '73d8d46dbee589'
|
120
|
+
def self.stop_machine app, machine
|
121
|
+
post "/v1/apps/#{app}/machines/#{machine}/stop"
|
122
|
+
end
|
123
|
+
|
124
|
+
# start_machine machine 'user-functions', '73d8d46dbee589'
|
125
|
+
def self.start_machine app, machine
|
126
|
+
post "/v1/apps/#{app}/machines/#{machine}/stop"
|
127
|
+
end
|
128
|
+
|
129
|
+
# delete_machine machine 'user-functions', '73d8d46dbee589'
|
130
|
+
def self.delete_machine app, machine
|
131
|
+
delete "/v1/apps/#{app}/machines/#{machine}"
|
132
|
+
end
|
133
|
+
|
134
|
+
# list_machines machine 'user-functions'
|
135
|
+
def self.list_machines app, machine
|
136
|
+
get "/v1/apps/#{app}/machines"
|
137
|
+
end
|
138
|
+
|
139
|
+
# delete_application 'user-functions'
|
140
|
+
def self.delete_application app, force=false
|
141
|
+
delete "/v1/apps/#{app}?force=#{force}"
|
142
|
+
end
|
143
|
+
|
144
|
+
# generic get
|
145
|
+
def self.get(path)
|
146
|
+
api(path) {|uri| request = Net::HTTP::Get.new(uri) }
|
147
|
+
end
|
148
|
+
|
149
|
+
# generic post
|
150
|
+
def self.post(path, hash=nil)
|
151
|
+
api(path) do |uri|
|
152
|
+
request = Net::HTTP::Post.new(uri)
|
153
|
+
request.body = hash.to_json if hash
|
154
|
+
request
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# generic delete
|
159
|
+
def self.delete(path)
|
160
|
+
api(path) {|uri| request = Net::HTTP::Delete.new(uri) }
|
161
|
+
end
|
162
|
+
|
163
|
+
# graphql -- see https://til.simonwillison.net/fly/undocumented-graphql-api
|
164
|
+
def self.graphql(query)
|
165
|
+
api('/graphql', 'https://api.fly.io/') do |path|
|
166
|
+
request = Net::HTTP::Post.new(path)
|
167
|
+
request.body = { query: query }.to_json
|
168
|
+
request
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# common processing for all APIs
|
173
|
+
def self.api(path, host=nil, &make_request)
|
174
|
+
host ||= "http://#{fly_api_hostname}"
|
175
|
+
uri = URI.join(host, path)
|
176
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
177
|
+
http.set_debug_output $stderr if ENV['TRACE']
|
178
|
+
http.use_ssl = true if uri.instance_of? URI::HTTPS
|
179
|
+
|
180
|
+
request = make_request.call(uri.request_uri)
|
181
|
+
|
182
|
+
@@api_token ||= `flyctl auth token`.chomp
|
183
|
+
headers = {
|
184
|
+
"Authorization" => "Bearer #{@@api_token}",
|
185
|
+
"Content-Type" => "application/json",
|
186
|
+
"Accept" => "application/json"
|
187
|
+
}
|
188
|
+
headers.each {|header, value| request[header] = value}
|
189
|
+
|
190
|
+
response = http.request(request)
|
191
|
+
|
192
|
+
if response.is_a? Net::HTTPSuccess
|
193
|
+
JSON.parse response.body, symbolize_names: true
|
194
|
+
else
|
195
|
+
body = response.body
|
196
|
+
begin
|
197
|
+
error = JSON.parse(body)
|
198
|
+
rescue
|
199
|
+
error = {body: body}
|
200
|
+
end
|
201
|
+
|
202
|
+
error[:status] = response.code
|
203
|
+
error[:message] = response.message
|
204
|
+
|
205
|
+
error
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module Fly
|
2
|
+
PLATFORMS = {
|
3
|
+
'Linux_arm64' => 'aarch64-linux',
|
4
|
+
'Linux_x86_64' => 'x86-linux',
|
5
|
+
'macOS_arm64' => 'arm64-darwin',
|
6
|
+
'macOS_x86_64' => 'x86_64-darwin',
|
7
|
+
'Windows_arm64' => nil, # Can't find a match
|
8
|
+
'Windows_x86_64' => 'x64-mingw32'
|
9
|
+
}
|
10
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Fly
|
2
|
+
module Scanner
|
3
|
+
# scan for major features - things that if present will likely affect
|
4
|
+
# more than one artifact that is generated.
|
5
|
+
def scan_rails_app
|
6
|
+
|
7
|
+
### database ###
|
8
|
+
|
9
|
+
database = YAML.load_file('config/database.yml').
|
10
|
+
dig('production', 'adapter') rescue nil
|
11
|
+
|
12
|
+
if database == 'sqlite3'
|
13
|
+
@sqlite3 = true
|
14
|
+
elsif database == 'postgresql'
|
15
|
+
@postgresql = true
|
16
|
+
elsif database == 'mysql' or database == 'mysql2'
|
17
|
+
@mysql = true
|
18
|
+
end
|
19
|
+
|
20
|
+
### ruby gems ###
|
21
|
+
|
22
|
+
@gemfile = []
|
23
|
+
|
24
|
+
if File.exist? 'Gemfile.lock'
|
25
|
+
parser = Bundler::LockfileParser.new(Bundler.read_file('Gemfile.lock'))
|
26
|
+
@gemfile += parser.specs.map { |spec, version| spec.name }
|
27
|
+
end
|
28
|
+
|
29
|
+
if File.exist? 'Gemfile'
|
30
|
+
@gemfile += Bundler::Definition.build('Gemfile', nil, []).dependencies.map(&:name)
|
31
|
+
end
|
32
|
+
|
33
|
+
@sidekiq = @gemfile.include? 'sidekiq'
|
34
|
+
@anycable = @gemfile.include? 'anycable-rails'
|
35
|
+
@rmagick = @gemfile.include? 'rmagick'
|
36
|
+
@image_processing = @gemfile.include? 'image_processing'
|
37
|
+
@bootstrap = @gemfile.include? 'bootstrap'
|
38
|
+
@puppeteer = @gemfile.include? 'puppeteer'
|
39
|
+
|
40
|
+
### node modules ###
|
41
|
+
|
42
|
+
@package_json = []
|
43
|
+
|
44
|
+
if File.exist? 'package.json'
|
45
|
+
@package_json += JSON.load_file('package.json')['dependencies'].keys rescue []
|
46
|
+
end
|
47
|
+
|
48
|
+
@puppeteer ||= @package_json.include? 'puppeteer'
|
49
|
+
|
50
|
+
### cable/redis ###
|
51
|
+
|
52
|
+
@cable = ! Dir['app/channels/*.rb'].empty?
|
53
|
+
|
54
|
+
if @cable
|
55
|
+
@redis_cable = true
|
56
|
+
if (YAML.load_file('config/cable.yml').dig('production', 'adapter') rescue '').include? 'any_cable'
|
57
|
+
@anycable = true
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
if (IO.read('config/environments/production.rb') =~ /redis/i rescue false)
|
62
|
+
@redis_cache = true
|
63
|
+
end
|
64
|
+
|
65
|
+
@redis = @redis_cable || @redis_cache || @sidekiq
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
begin
|
2
|
+
require 'pty'
|
3
|
+
rescue LoadError
|
4
|
+
# Presumably Windows
|
5
|
+
end
|
6
|
+
|
7
|
+
module FlyIoRails
|
8
|
+
module Utils
|
9
|
+
|
10
|
+
def tee cmd
|
11
|
+
say_status :run, cmd if defined? say_status
|
12
|
+
FlyIoRails::Utils.tee cmd
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.tee cmd
|
16
|
+
data = []
|
17
|
+
|
18
|
+
if defined? PTY
|
19
|
+
begin
|
20
|
+
# PTY supports ANSI cursor control and colors
|
21
|
+
PTY.spawn(cmd) do |read, write, pid|
|
22
|
+
begin
|
23
|
+
read.each do |line|
|
24
|
+
print line
|
25
|
+
data << line
|
26
|
+
end
|
27
|
+
rescue Errno::EIO
|
28
|
+
end
|
29
|
+
end
|
30
|
+
rescue PTY::ChildExited
|
31
|
+
end
|
32
|
+
else
|
33
|
+
# no support for ANSI cursor control and colors
|
34
|
+
Open3.popen2e(cmd) do |stdin, out, thread|
|
35
|
+
out.each do |line|
|
36
|
+
print line
|
37
|
+
data << line
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
data.join
|
43
|
+
end
|
44
|
+
|
45
|
+
def create_app(name: nil, org: 'personal', regions: [], nomad: false, **rest)
|
46
|
+
cmd = if name
|
47
|
+
"flyctl apps create #{name.inspect} --org #{org.inspect} --machines"
|
48
|
+
else
|
49
|
+
"flyctl apps create --generate-name --org #{org.inspect} --machines"
|
50
|
+
end
|
51
|
+
|
52
|
+
cmd.sub! ' --machines', '' if nomad
|
53
|
+
|
54
|
+
output = tee cmd
|
55
|
+
exit 1 unless output =~ /^New app created: /
|
56
|
+
|
57
|
+
@app = output.split.last
|
58
|
+
|
59
|
+
unless regions.empty?
|
60
|
+
@regions = regions.flatten
|
61
|
+
end
|
62
|
+
|
63
|
+
@app
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/fly-rails.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'rails'
|
2
|
+
require 'fly-rails/generators'
|
3
|
+
require 'fly-rails/utils'
|
4
|
+
|
5
|
+
class FlyIoRailtie < Rails::Railtie
|
6
|
+
# load rake tasks
|
7
|
+
rake_tasks do
|
8
|
+
Dir[File.expand_path('tasks/*.rake', __dir__)].each do |file|
|
9
|
+
load file
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# set FLY_IMAGE_NAME on Nomad VMs
|
14
|
+
if not ENV['FLY_IMAGE_REF'] and ENV['FLY_APP_NAME'] and ENV['FLY_API_TOKEN']
|
15
|
+
require 'fly-rails/machines'
|
16
|
+
|
17
|
+
ENV['FLY_IMAGE_REF'] = Fly::Machines.graphql(%{
|
18
|
+
query {
|
19
|
+
app(name: "#{ENV['FLY_APP_NAME']}") {
|
20
|
+
currentRelease {
|
21
|
+
imageRef
|
22
|
+
}
|
23
|
+
}
|
24
|
+
}
|
25
|
+
}).dig(:data, :app, :currentRelease, :imageRef)
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'fly-rails/actions'
|
2
|
+
|
3
|
+
module Fly::Generators
|
4
|
+
class AppGenerator < Rails::Generators::Base
|
5
|
+
include FlyIoRails::Utils
|
6
|
+
|
7
|
+
class_option :name, type: :string, required: false
|
8
|
+
class_option :org, type: :string, default: 'personal'
|
9
|
+
class_option :region, type: :array, repeatable: true, default: []
|
10
|
+
class_option :nomad, type: :boolean, default: false
|
11
|
+
class_option :eject, type: :boolean, default: nil
|
12
|
+
|
13
|
+
class_option :anycable, type: :boolean, default: false
|
14
|
+
class_option :avahi, type: :boolean, default: false
|
15
|
+
class_option :litefs, type: :boolean, default: false
|
16
|
+
class_option :nats, type: :boolean, default: false
|
17
|
+
class_option :redis, type: :boolean, default: false
|
18
|
+
class_option :passenger, type: :boolean, default: false
|
19
|
+
class_option :serverless, type: :boolean, default: false
|
20
|
+
|
21
|
+
def generate_app
|
22
|
+
source_paths.push File.expand_path('../templates', __dir__)
|
23
|
+
|
24
|
+
# the plan is to make eject an option, default to false, but until
|
25
|
+
# that is ready, have generate fly:app always eject
|
26
|
+
opts = options.to_h.symbolize_keys
|
27
|
+
opts[:eject] = opts[:nomad] if opts[:eject] == nil
|
28
|
+
|
29
|
+
if File.exist? 'fly.toml'
|
30
|
+
toml = TOML.load_file('fly.toml')
|
31
|
+
opts[:name] ||= toml['app']
|
32
|
+
apps = JSON.parse(`flyctl list apps --json`) rescue []
|
33
|
+
|
34
|
+
if toml.keys.length == 1
|
35
|
+
if opts[:name] != toml['app']
|
36
|
+
# replace existing fly.toml
|
37
|
+
File.unlink 'fly.toml'
|
38
|
+
create_app(**opts.symbolize_keys)
|
39
|
+
elsif not apps.any? {|item| item['ID'] == opts[:name]}
|
40
|
+
create_app(**opts.symbolize_keys)
|
41
|
+
end
|
42
|
+
elsif opts[:name] != toml['app']
|
43
|
+
say_status "fly:app", "Using the name in the existing toml file", :red
|
44
|
+
opts[:name] = toml['app']
|
45
|
+
end
|
46
|
+
else
|
47
|
+
create_app(**opts.symbolize_keys)
|
48
|
+
end
|
49
|
+
|
50
|
+
action = Fly::Actions.new(@app, opts)
|
51
|
+
|
52
|
+
action.generate_toml
|
53
|
+
action.generate_fly_config unless File.exist? 'config/fly.rb'
|
54
|
+
|
55
|
+
if opts[:eject]
|
56
|
+
action.generate_dockerfile
|
57
|
+
action.generate_dockerignore unless File.exist? '.dockerignore'
|
58
|
+
action.generate_nginx_conf unless File.exist? 'config/nginx.conf'
|
59
|
+
action.generate_raketask unless File.exist? 'lib/tasks/fly.rake'
|
60
|
+
action.generate_procfile unless File.exist? 'Procfile.rake'
|
61
|
+
action.generate_litefs if opts[:litefs] and not File.exist? 'config/litefs'
|
62
|
+
action.generate_patches
|
63
|
+
end
|
64
|
+
|
65
|
+
ips = `flyctl ips list`.strip.lines[1..].map(&:split).map(&:first)
|
66
|
+
action.generate_ipv4 unless ips.include? 'v4'
|
67
|
+
action.generate_ipv6 unless ips.include? 'v6'
|
68
|
+
|
69
|
+
action.launch(action.app)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'fly-rails/actions'
|
2
|
+
|
3
|
+
module Fly::Generators
|
4
|
+
class ConfigGenerator < Rails::Generators::Base
|
5
|
+
include FlyIoRails::Utils
|
6
|
+
|
7
|
+
# despite its name, this is a debug tool that will dump the config
|
8
|
+
def generate_config
|
9
|
+
action = Fly::Actions.new(@app, options)
|
10
|
+
|
11
|
+
config = {}
|
12
|
+
action.instance_variables.sort.each do |name|
|
13
|
+
config[name] = action.instance_variable_get(name)
|
14
|
+
end
|
15
|
+
|
16
|
+
pp config
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|