fly-rails 0.3.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|