fly.io-rails 0.0.8 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a3bb276f91a3517a89d96e6416e3944fd05c5127e30fbf21ff999e194d3b368d
4
- data.tar.gz: 1ff540bed90a499253a1c0b5ed9d5418b4284378396c532a0dbf1cf05e2d3e2e
3
+ metadata.gz: 2ce085fcf4d7d13a42d47f0eade7d614af8756e66bd15093d2c35d8caf99138c
4
+ data.tar.gz: cda5dd640d9a694c0e1db2510c85ade09a1718bd0662fb527dd20d51a10fbd66
5
5
  SHA512:
6
- metadata.gz: 5a60a56d8468e1f4f434bd45da75a08f5ff918554aa5241799abf681667b938f32ae330fe2b166992c55448681d06ac6d7bc9176cd4fe912ab1e825738644dca
7
- data.tar.gz: 60785bbab16177bc41402518eba1146e44db3f4fda50726f60166888728618ed1496e00242bbbb8d4b8dd8113697ded35a4a5ff785f522f46c685dd491bdbb64
6
+ metadata.gz: 8f2ccb5c30c8470e3a841057c558ae312fc9ec4439568e1125f6ce8555a99f43bfd4b4b0360e54badb4912da1b7bc39881ccf639eadefa10f3a1ab4fe1c8263a
7
+ data.tar.gz: e8d704a3685868da001b3c66abd549c31cac43e77889d2ed219c410dc9914421643c9fb8e78342734965721f9518bc532f96db749d9e86669017f7c7768d4efb
@@ -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,192 @@
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 = nil
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 fly api hostname. Starts proxy if necessary
32
+ def self.fly_api_hostname!
33
+ hostname = fly_api_hostname
34
+ return hostname if hostname
35
+
36
+ org = 'personal'
37
+
38
+ if File.exist? 'fly.toml'
39
+ require 'toml'
40
+ app = TOML.load_file('fly.toml')['app']
41
+
42
+ apps = JSON.parse(`flyctl list apps --json`) rescue []
43
+
44
+ apps.each do |info|
45
+ org = info['Organization'] if info['ID'] == app
46
+ end
47
+ end
48
+
49
+ pid = fork { exec "flyctl machines api-proxy --org #{org}" }
50
+ at_exit { Process.kill "INT", pid }
51
+
52
+ # wait up to 12.7 seconds for the proxy to start
53
+ wait = 0.1
54
+ 6.times do
55
+ sleep wait
56
+ begin
57
+ Net::HTTP.get URI('http://127.0.0.1:4280')
58
+ @@fly_api_hostname = '127.0.0.1:4280'
59
+ break
60
+ rescue
61
+ wait *= 2
62
+ end
63
+ end
64
+
65
+ @@fy_api_hostname
66
+ end
67
+
68
+ # create_fly_application app_name: 'user-functions', org_slug: 'personal'
69
+ def self.create_fly_application options
70
+ post '/v1/apps', options
71
+ end
72
+
73
+ # get_application_details 'user-functions'
74
+ def self.get_application_details app
75
+ get "/v1/apps/#{app}"
76
+ end
77
+
78
+ # create_start_machine 'user-functions', name: 'quirky_machine', config: {
79
+ # image: 'flyio/fastify-functions',
80
+ # env: {'APP_ENV' => 'production'},
81
+ # services: [
82
+ # {
83
+ # ports: [
84
+ # {port: 443, handlers: ['tls', 'http']},
85
+ # {port: 80, handlers: ['http']}
86
+ # ],
87
+ # protocol: 'tcp',
88
+ # internal_protocol: 'tcp',
89
+ # }
90
+ # ]
91
+ # }
92
+ def self.create_start_machine app, options
93
+ post "/v1/apps/#{app}/machines", options
94
+ end
95
+
96
+ # wait_for_machine_to_start 'user-functions', '73d8d46dbee589'
97
+ def self.wait_for_machine_to_start app, machine, timeout=60
98
+ get "/v1/apps/#{app}/machines/#{machine}/wait?timeout=#{timeout}"
99
+ end
100
+
101
+ # get_a_machine machine 'user-functions', '73d8d46dbee589'
102
+ def self.get_a_machine app, machine
103
+ get "/v1/apps/#{app}/machines/#{machine}"
104
+ end
105
+
106
+ # update_a_machine 'user-functions', '73d8d46dbee589', config: {
107
+ # image: 'flyio/fastify-functions',
108
+ # guest: { memory_mb: 512, cpus: 2 }
109
+ # }
110
+ def self.update_a_machine app, machine, options
111
+ post "/v1/apps/#{app}/machines/#{machine}", options
112
+ end
113
+
114
+ # stop_machine machine 'user-functions', '73d8d46dbee589'
115
+ def self.stop_machine app, machine
116
+ post "/v1/apps/#{app}/machines/#{machine}/stop"
117
+ end
118
+
119
+ # start_machine machine 'user-functions', '73d8d46dbee589'
120
+ def self.start_machine app, machine
121
+ post "/v1/apps/#{app}/machines/#{machine}/stop"
122
+ end
123
+
124
+ # delete_machine machine 'user-functions', '73d8d46dbee589'
125
+ def self.delete_machine app, machine
126
+ delete "/v1/apps/#{app}/machines/#{machine}"
127
+ end
128
+
129
+ # list_machines machine 'user-functions'
130
+ def self.list_machines app, machine
131
+ get "/v1/apps/#{app}/machines"
132
+ end
133
+
134
+ # delete_application 'user-functions'
135
+ def self.delete_application app, force=false
136
+ delete "/v1/apps/#{app}?force=#{force}"
137
+ end
138
+
139
+ # generic get
140
+ def self.get(path)
141
+ api(path) {|uri| request = Net::HTTP::Get.new(uri) }
142
+ end
143
+
144
+ # generic post
145
+ def self.post(path, hash=nil)
146
+ api(path) do |uri|
147
+ request = Net::HTTP::Post.new(uri)
148
+ request.body = hash.to_json if hash
149
+ request
150
+ end
151
+ end
152
+
153
+ # generic delete
154
+ def self.delete(path)
155
+ api(path) {|uri| request = Net::HTTP::Delete.new(uri) }
156
+ end
157
+
158
+ # common processing for all APIs
159
+ def self.api(path, &make_request)
160
+ uri = URI("http://#{fly_api_hostname}#{path}")
161
+ http = Net::HTTP.new(uri.host, uri.port)
162
+
163
+ request = make_request.call(uri.request_uri)
164
+
165
+ @@api_token ||= `fly auth token`.chomp
166
+ headers = {
167
+ "Authorization" => "Bearer #{@@api_token}",
168
+ "Content-Type" => "application/json",
169
+ "Accept" => "application/json"
170
+ }
171
+ headers.each {|header, value| request[header] = value}
172
+
173
+ response = http.request(request)
174
+
175
+ if response.is_a? Net::HTTPSuccess
176
+ JSON.parse response.body, symbolize_names: true
177
+ else
178
+ body = response.body
179
+ begin
180
+ error = JSON.parse(body)
181
+ rescue
182
+ error = {body: body}
183
+ end
184
+
185
+ error[:status] = response.code
186
+ error[:message] = response.message
187
+
188
+ error
189
+ end
190
+ end
191
+ end
192
+ end
@@ -1,3 +1,3 @@
1
1
  module Fly_io
2
- VERSION = '0.0.8'
2
+ VERSION = '0.0.9'
3
3
  end
data/lib/tasks/fly.rake CHANGED
@@ -1,16 +1,84 @@
1
+ require 'fly.io-rails/machines'
2
+ require 'fly.io-rails/hcl'
3
+
1
4
  namespace :fly do
2
5
  desc 'Deploy fly application'
3
6
  task :deploy do
7
+ # build and push an image
4
8
  out = FlyIoRails::Utils.tee 'fly deploy --build-only --push'
5
9
  image = out[/image:\s+(.*)/, 1]
6
10
 
7
- if image
8
- tf = IO.read('main.tf')
9
- tf[/^\s*image\s*=\s*"(.*?)"/, 1] = image.strip
10
- IO.write 'main.tf', tf
11
+ exit 1 unless image
12
+
13
+ # update main.tf with the image name
14
+ tf = IO.read('main.tf')
15
+ tf[/^\s*image\s*=\s*"(.*?)"/, 1] = image.strip
16
+ IO.write 'main.tf', tf
17
+
18
+ # find first machine in terraform config file
19
+ machines = Fly::HCL.parse IO.read('main.tf').find {|block|
20
+ block.keys.first == :resource and
21
+ block.values.first.keys.first == 'fly_machine'}
22
+
23
+ # extract HCL configuration for the machine
24
+ config = machines.values.first.values.first.values.first
25
+
26
+ # extract fly application name
27
+ app = config[:app]
28
+
29
+ # delete HCL specific configuration items
30
+ %i(services for_each region app name depends_on).each do |key|
31
+ config.delete key
32
+ end
33
+
34
+ # move machine configuration into guest object
35
+ config[:guest] = {
36
+ cpus: config.delete(:cpus),
37
+ memory_mb: config.delete(:memorymb),
38
+ cpu_kind: config.delete(:cputype)
39
+ }
40
+
41
+ # release machines should have no services or mounts
42
+ config.delete :services
43
+ config.delete :mounts
44
+
45
+ # override start command
46
+ config[:env]['SERVER_COMMAND'] = 'bin/rails fly:release'
47
+
48
+ # start release machine
49
+ start = Fly::Machines.create_start_machine(app, config: config)
50
+ machine = start[:id]
51
+
52
+ if !machine
53
+ STDERR.puts 'Error starting release machine'
54
+ PP.pp start, STDERR
55
+ exit 1
56
+ end
57
+
58
+ # wait for release to copmlete
59
+ event = nil
60
+ 90.times do
61
+ sleep 1
62
+ status = Fly::Machines.get_a_machine app, machine
63
+ event = status[:events]&.first
64
+ break if event && event[:type] == 'exit'
65
+ end
66
+
67
+ # extract exit code
68
+ exit_code = event.dig(:request, :exit_event, :exit_code)
69
+
70
+ if exit_code == 0
71
+ # delete release machine
72
+ Fly::Machines.delete_machine app, machine
11
73
 
74
+ # use terraform apply to deploy
12
75
  ENV['FLY_API_TOKEN'] = `flyctl auth token`.chomp
13
76
  system 'terraform apply -auto-approve'
77
+ else
78
+ STDERR.puts 'Error performing release'
79
+ STDERR.puts (exit_code ? {exit_code: exit_code} : event).inspect
80
+ STDERR.puts "run 'flyctl logs --instance #{machine}' for more information"
81
+ exit 1
14
82
  end
15
83
  end
16
84
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fly.io-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.8
4
+ version: 0.0.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Ruby
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-09-17 00:00:00.000000000 Z
11
+ date: 2022-09-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: fly-ruby
@@ -37,6 +37,8 @@ files:
37
37
  - exe/flyctl
38
38
  - lib/fly.io-rails.rb
39
39
  - lib/fly.io-rails/generators.rb
40
+ - lib/fly.io-rails/hcl.rb
41
+ - lib/fly.io-rails/machines.rb
40
42
  - lib/fly.io-rails/platforms.rb
41
43
  - lib/fly.io-rails/utils.rb
42
44
  - lib/fly.io-rails/version.rb