railsdocks 1.0.0
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/exe/railsdocks +6 -0
- data/lib/railsdocks/cli.rb +762 -0
- data/lib/railsdocks/client.rb +233 -0
- data/lib/railsdocks/config.rb +47 -0
- data/lib/railsdocks/ssh_config.rb +85 -0
- data/lib/railsdocks/version.rb +5 -0
- data/lib/railsdocks.rb +7 -0
- metadata +60 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: eae59bfa42a70c9c629c3d43d0af0b41f42ba79b492f68bdbfc0498e8513560d
|
|
4
|
+
data.tar.gz: 20f46b9fbc1ee330e278711e2d0784eee88674cd60fe92534f6f37b5298576c8
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 4427a2ffaf198c320bcc9e1bf43effc58b71e80f68a37591d5eaefd3d3488b063de4f64cd954e900e7cda242f8bb57863df5a202b7513022e1f2e516e843668b
|
|
7
|
+
data.tar.gz: 5903433c21dc4ee9d5717672faae4db2e38ec41f91c1f79b94c6804bd65c0b76d17faf5ba11025a87ffd566ac478ac34fb052c80390474b57ded175a64d08ef4
|
data/exe/railsdocks
ADDED
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "io/console"
|
|
4
|
+
require "open3"
|
|
5
|
+
|
|
6
|
+
module RailsDocks
|
|
7
|
+
class CLI
|
|
8
|
+
def self.start(arguments)
|
|
9
|
+
command = arguments.shift
|
|
10
|
+
|
|
11
|
+
case command
|
|
12
|
+
when "login"
|
|
13
|
+
login
|
|
14
|
+
when "setup"
|
|
15
|
+
setup
|
|
16
|
+
when "apps:create"
|
|
17
|
+
create(arguments)
|
|
18
|
+
when "apps:list"
|
|
19
|
+
apps_list
|
|
20
|
+
when "apps:destroy"
|
|
21
|
+
apps_destroy(arguments)
|
|
22
|
+
when "db:migrate"
|
|
23
|
+
database_command("migrate")
|
|
24
|
+
when "db:rollback"
|
|
25
|
+
database_command("rollback")
|
|
26
|
+
when "domains:list"
|
|
27
|
+
domains_list
|
|
28
|
+
when "domains:add"
|
|
29
|
+
domains_add(arguments)
|
|
30
|
+
when "domains:remove"
|
|
31
|
+
domains_remove(arguments)
|
|
32
|
+
when "version", "--version", "-v"
|
|
33
|
+
puts "RailsDocks #{RailsDocks::VERSION}"
|
|
34
|
+
0
|
|
35
|
+
when nil, "help", "--help", "-h"
|
|
36
|
+
help
|
|
37
|
+
else
|
|
38
|
+
warn "Unknown command: #{command}"
|
|
39
|
+
warn "Run `railsdocks help` for available commands."
|
|
40
|
+
1
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.login
|
|
45
|
+
print "Email: "
|
|
46
|
+
|
|
47
|
+
email =
|
|
48
|
+
$stdin.gets&.chomp.to_s
|
|
49
|
+
|
|
50
|
+
print "Password: "
|
|
51
|
+
|
|
52
|
+
password =
|
|
53
|
+
$stdin.noecho(&:gets)&.chomp.to_s
|
|
54
|
+
|
|
55
|
+
puts
|
|
56
|
+
|
|
57
|
+
response =
|
|
58
|
+
Client.new.login(
|
|
59
|
+
email: email,
|
|
60
|
+
password: password
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
Config.save_login(
|
|
64
|
+
email:
|
|
65
|
+
response
|
|
66
|
+
.fetch("user")
|
|
67
|
+
.fetch("email"),
|
|
68
|
+
token:
|
|
69
|
+
response.fetch("token")
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
SSHConfig.configure
|
|
73
|
+
|
|
74
|
+
puts(
|
|
75
|
+
"Logged in as " \
|
|
76
|
+
"#{response.fetch("user").fetch("email")}."
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
puts "RailsDocks SSH configured."
|
|
80
|
+
|
|
81
|
+
0
|
|
82
|
+
rescue Client::Error => e
|
|
83
|
+
warn "Login failed: #{e.message}"
|
|
84
|
+
1
|
|
85
|
+
rescue SSHConfig::Error => e
|
|
86
|
+
warn(
|
|
87
|
+
"Login succeeded, but SSH setup failed: " \
|
|
88
|
+
"#{e.message}"
|
|
89
|
+
)
|
|
90
|
+
1
|
|
91
|
+
rescue KeyError
|
|
92
|
+
warn(
|
|
93
|
+
"Login failed: RailsDocks returned " \
|
|
94
|
+
"an unexpected response."
|
|
95
|
+
)
|
|
96
|
+
1
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def self.setup
|
|
100
|
+
SSHConfig.configure
|
|
101
|
+
|
|
102
|
+
puts "RailsDocks SSH configured."
|
|
103
|
+
puts "You can now deploy with:"
|
|
104
|
+
puts
|
|
105
|
+
puts " git push railsdocks main"
|
|
106
|
+
|
|
107
|
+
0
|
|
108
|
+
rescue SSHConfig::Error => e
|
|
109
|
+
warn "Setup failed: #{e.message}"
|
|
110
|
+
1
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def self.create(arguments)
|
|
114
|
+
name = nil
|
|
115
|
+
no_db = false
|
|
116
|
+
|
|
117
|
+
until arguments.empty?
|
|
118
|
+
argument =
|
|
119
|
+
arguments.shift
|
|
120
|
+
|
|
121
|
+
case argument
|
|
122
|
+
when "--no-db", "-no-db"
|
|
123
|
+
no_db = true
|
|
124
|
+
else
|
|
125
|
+
if argument.start_with?("-")
|
|
126
|
+
warn "Unknown option: #{argument}"
|
|
127
|
+
return 1
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
if name
|
|
131
|
+
warn "Unexpected argument: #{argument}"
|
|
132
|
+
warn(
|
|
133
|
+
"Usage: railsdocks apps:create APP_NAME [--no-db]"
|
|
134
|
+
)
|
|
135
|
+
return 1
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
name = argument
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
if name.to_s.strip.empty?
|
|
143
|
+
warn(
|
|
144
|
+
"Usage: railsdocks apps:create APP_NAME [--no-db]"
|
|
145
|
+
)
|
|
146
|
+
return 1
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
unless Config.logged_in?
|
|
150
|
+
warn "You are not logged in."
|
|
151
|
+
warn "Run `railsdocks login` first."
|
|
152
|
+
return 1
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
SSHConfig.configure
|
|
156
|
+
|
|
157
|
+
response =
|
|
158
|
+
Client.new.create_app(
|
|
159
|
+
name: name,
|
|
160
|
+
token: Config.token,
|
|
161
|
+
no_db: no_db
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
app =
|
|
165
|
+
response.fetch("app")
|
|
166
|
+
|
|
167
|
+
git_url =
|
|
168
|
+
app.fetch("git_url")
|
|
169
|
+
|
|
170
|
+
puts "Created #{app.fetch("slug")}."
|
|
171
|
+
|
|
172
|
+
puts(
|
|
173
|
+
"Hostname: https://" \
|
|
174
|
+
"#{app.fetch("hostname")}"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
if no_db
|
|
178
|
+
puts "Database: disabled"
|
|
179
|
+
else
|
|
180
|
+
puts "Database: PostgreSQL"
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
if inside_git_repository?
|
|
184
|
+
add_git_remote(git_url)
|
|
185
|
+
else
|
|
186
|
+
puts
|
|
187
|
+
puts "Not currently inside a Git repository."
|
|
188
|
+
puts "Add the RailsDocks remote when ready:"
|
|
189
|
+
puts
|
|
190
|
+
puts(
|
|
191
|
+
" git remote add railsdocks #{git_url}"
|
|
192
|
+
)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
0
|
|
196
|
+
rescue Client::Error => e
|
|
197
|
+
warn "Create failed: #{e.message}"
|
|
198
|
+
1
|
|
199
|
+
rescue SSHConfig::Error => e
|
|
200
|
+
warn "Create failed: #{e.message}"
|
|
201
|
+
1
|
|
202
|
+
rescue KeyError
|
|
203
|
+
warn(
|
|
204
|
+
"Create failed: RailsDocks returned " \
|
|
205
|
+
"an unexpected response."
|
|
206
|
+
)
|
|
207
|
+
1
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def self.apps_list
|
|
211
|
+
unless Config.logged_in?
|
|
212
|
+
warn "You are not logged in."
|
|
213
|
+
warn "Run `railsdocks login` first."
|
|
214
|
+
return 1
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
response =
|
|
218
|
+
Client.new.apps(
|
|
219
|
+
token: Config.token
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
apps =
|
|
223
|
+
response.fetch("apps")
|
|
224
|
+
|
|
225
|
+
if apps.empty?
|
|
226
|
+
puts "No applications."
|
|
227
|
+
return 0
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
name_width =
|
|
231
|
+
[
|
|
232
|
+
"Name".length,
|
|
233
|
+
*apps.map {
|
|
234
|
+
|app|
|
|
235
|
+
app.fetch("name").length
|
|
236
|
+
}
|
|
237
|
+
].max
|
|
238
|
+
|
|
239
|
+
hostname_width =
|
|
240
|
+
[
|
|
241
|
+
"Hostname".length,
|
|
242
|
+
*apps.map {
|
|
243
|
+
|app|
|
|
244
|
+
app.fetch("hostname").length
|
|
245
|
+
}
|
|
246
|
+
].max
|
|
247
|
+
|
|
248
|
+
printf(
|
|
249
|
+
"%-#{name_width}s %-#{hostname_width}s %s\n",
|
|
250
|
+
"Name",
|
|
251
|
+
"Hostname",
|
|
252
|
+
"Database"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
apps.each do |app|
|
|
256
|
+
database =
|
|
257
|
+
app.fetch("database_enabled") ?
|
|
258
|
+
"PostgreSQL" :
|
|
259
|
+
"None"
|
|
260
|
+
|
|
261
|
+
printf(
|
|
262
|
+
"%-#{name_width}s %-#{hostname_width}s %s\n",
|
|
263
|
+
app.fetch("name"),
|
|
264
|
+
app.fetch("hostname"),
|
|
265
|
+
database
|
|
266
|
+
)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
0
|
|
270
|
+
rescue Client::Error => e
|
|
271
|
+
warn "Could not list applications: #{e.message}"
|
|
272
|
+
1
|
|
273
|
+
rescue KeyError
|
|
274
|
+
warn(
|
|
275
|
+
"Could not list applications: RailsDocks returned " \
|
|
276
|
+
"an unexpected response."
|
|
277
|
+
)
|
|
278
|
+
1
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def self.apps_destroy(arguments)
|
|
282
|
+
slug =
|
|
283
|
+
arguments.shift.to_s.strip
|
|
284
|
+
|
|
285
|
+
if slug.empty? ||
|
|
286
|
+
!arguments.empty?
|
|
287
|
+
warn(
|
|
288
|
+
"Usage: railsdocks apps:destroy APP_NAME"
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
return 1
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
unless Config.logged_in?
|
|
295
|
+
warn "You are not logged in."
|
|
296
|
+
warn "Run `railsdocks login` first."
|
|
297
|
+
return 1
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
puts(
|
|
301
|
+
"Destroy #{slug}? This cannot be undone."
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
print "Type #{slug} to confirm: "
|
|
305
|
+
|
|
306
|
+
confirmation =
|
|
307
|
+
$stdin.gets&.chomp.to_s
|
|
308
|
+
|
|
309
|
+
unless confirmation == slug
|
|
310
|
+
puts "Cancelled."
|
|
311
|
+
return 1
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
response =
|
|
315
|
+
Client.new.destroy_app(
|
|
316
|
+
slug: slug,
|
|
317
|
+
confirm: confirmation,
|
|
318
|
+
token: Config.token
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
app =
|
|
322
|
+
response.fetch("app")
|
|
323
|
+
|
|
324
|
+
destroyed_slug =
|
|
325
|
+
app.fetch("slug")
|
|
326
|
+
|
|
327
|
+
unless app.fetch("destroyed") == true
|
|
328
|
+
raise KeyError,
|
|
329
|
+
"destroyed"
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
puts "Destroyed #{destroyed_slug}."
|
|
333
|
+
|
|
334
|
+
0
|
|
335
|
+
rescue Client::Error => e
|
|
336
|
+
warn "Destroy failed: #{e.message}"
|
|
337
|
+
1
|
|
338
|
+
rescue KeyError
|
|
339
|
+
warn(
|
|
340
|
+
"Destroy failed: RailsDocks returned " "an unexpected response."
|
|
341
|
+
)
|
|
342
|
+
1
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def self.database_command(operation)
|
|
346
|
+
unless Config.logged_in?
|
|
347
|
+
warn "You are not logged in."
|
|
348
|
+
warn "Run `railsdocks login` first."
|
|
349
|
+
return 1
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
slug =
|
|
353
|
+
current_app_slug
|
|
354
|
+
|
|
355
|
+
unless slug
|
|
356
|
+
warn(
|
|
357
|
+
"Could not determine the RailsDocks app."
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
warn(
|
|
361
|
+
"Run this command from a Git repository " \
|
|
362
|
+
"with a `railsdocks` remote."
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
return 1
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
display_name =
|
|
369
|
+
operation == "migrate" ?
|
|
370
|
+
"Migrating" :
|
|
371
|
+
"Rolling back"
|
|
372
|
+
|
|
373
|
+
puts "#{display_name} #{slug}..."
|
|
374
|
+
|
|
375
|
+
response =
|
|
376
|
+
Client.new.database_command(
|
|
377
|
+
slug: slug,
|
|
378
|
+
operation: operation,
|
|
379
|
+
token: Config.token
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
output =
|
|
383
|
+
response["output"].to_s.strip
|
|
384
|
+
|
|
385
|
+
puts output unless output.empty?
|
|
386
|
+
|
|
387
|
+
puts(
|
|
388
|
+
operation == "migrate" ?
|
|
389
|
+
"Migration complete." :
|
|
390
|
+
"Rollback complete."
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
0
|
|
394
|
+
rescue Client::Error => e
|
|
395
|
+
warn(
|
|
396
|
+
"Database command failed: #{e.message}"
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
1
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def self.domains_list
|
|
403
|
+
unless Config.logged_in?
|
|
404
|
+
warn "You are not logged in."
|
|
405
|
+
warn "Run `railsdocks login` first."
|
|
406
|
+
return 1
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
response =
|
|
410
|
+
Client.new.domains(
|
|
411
|
+
token: Config.token
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
domains =
|
|
415
|
+
response.fetch("domains")
|
|
416
|
+
|
|
417
|
+
if domains.empty?
|
|
418
|
+
puts "No custom domains."
|
|
419
|
+
return 0
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
hostname_width =
|
|
423
|
+
[
|
|
424
|
+
"Domain".length,
|
|
425
|
+
*domains.map {
|
|
426
|
+
|domain|
|
|
427
|
+
domain.fetch("hostname").length
|
|
428
|
+
}
|
|
429
|
+
].max
|
|
430
|
+
|
|
431
|
+
app_width =
|
|
432
|
+
[
|
|
433
|
+
"App".length,
|
|
434
|
+
*domains.map {
|
|
435
|
+
|domain|
|
|
436
|
+
domain.fetch("app").length
|
|
437
|
+
}
|
|
438
|
+
].max
|
|
439
|
+
|
|
440
|
+
printf(
|
|
441
|
+
"%-#{hostname_width}s %-#{app_width}s %s\n",
|
|
442
|
+
"Domain",
|
|
443
|
+
"App",
|
|
444
|
+
"Status"
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
domains.each do |domain|
|
|
448
|
+
printf(
|
|
449
|
+
"%-#{hostname_width}s %-#{app_width}s %s\n",
|
|
450
|
+
domain.fetch("hostname"),
|
|
451
|
+
domain.fetch("app"),
|
|
452
|
+
domain.fetch("status")
|
|
453
|
+
)
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
0
|
|
457
|
+
rescue Client::Error => e
|
|
458
|
+
warn "Could not list domains: #{e.message}"
|
|
459
|
+
1
|
|
460
|
+
rescue KeyError
|
|
461
|
+
warn(
|
|
462
|
+
"Could not list domains: RailsDocks returned " \
|
|
463
|
+
"an unexpected response."
|
|
464
|
+
)
|
|
465
|
+
1
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def self.domains_add(arguments)
|
|
469
|
+
unless Config.logged_in?
|
|
470
|
+
warn "You are not logged in."
|
|
471
|
+
warn "Run `railsdocks login` first."
|
|
472
|
+
return 1
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
hostname =
|
|
476
|
+
arguments.shift.to_s.strip
|
|
477
|
+
|
|
478
|
+
if hostname.empty?
|
|
479
|
+
warn(
|
|
480
|
+
"Usage: railsdocks domains:add DOMAIN " \
|
|
481
|
+
"[--app APP_NAME]"
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
return 1
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
app_slug = nil
|
|
488
|
+
|
|
489
|
+
until arguments.empty?
|
|
490
|
+
argument =
|
|
491
|
+
arguments.shift
|
|
492
|
+
|
|
493
|
+
case argument
|
|
494
|
+
when "--app"
|
|
495
|
+
app_slug =
|
|
496
|
+
arguments.shift.to_s.strip
|
|
497
|
+
|
|
498
|
+
if app_slug.empty?
|
|
499
|
+
warn "--app requires an application name."
|
|
500
|
+
return 1
|
|
501
|
+
end
|
|
502
|
+
else
|
|
503
|
+
warn "Unknown option: #{argument}"
|
|
504
|
+
return 1
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
app_slug ||=
|
|
509
|
+
current_app_slug
|
|
510
|
+
|
|
511
|
+
unless app_slug
|
|
512
|
+
warn(
|
|
513
|
+
"Could not determine the RailsDocks app."
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
warn(
|
|
517
|
+
"Use --app APP_NAME or run this command " \
|
|
518
|
+
"from a RailsDocks Git repository."
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
return 1
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
response =
|
|
525
|
+
Client.new.add_domain(
|
|
526
|
+
hostname: hostname,
|
|
527
|
+
app: app_slug,
|
|
528
|
+
token: Config.token
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
domain =
|
|
532
|
+
response.fetch("domain")
|
|
533
|
+
|
|
534
|
+
dns =
|
|
535
|
+
response.fetch("dns")
|
|
536
|
+
|
|
537
|
+
puts(
|
|
538
|
+
"Added #{domain.fetch("hostname")} " \
|
|
539
|
+
"to #{domain.fetch("app")}."
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
puts
|
|
543
|
+
puts "DNS configuration:"
|
|
544
|
+
|
|
545
|
+
puts(
|
|
546
|
+
" #{dns.fetch("type")} " \
|
|
547
|
+
"#{dns.fetch("name")} " \
|
|
548
|
+
"#{dns.fetch("target")}"
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
puts
|
|
552
|
+
|
|
553
|
+
puts(
|
|
554
|
+
"Status: #{domain.fetch("status")}"
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
0
|
|
558
|
+
rescue Client::Error => e
|
|
559
|
+
warn "Could not add domain: #{e.message}"
|
|
560
|
+
1
|
|
561
|
+
rescue KeyError
|
|
562
|
+
warn(
|
|
563
|
+
"Could not add domain: RailsDocks returned " \
|
|
564
|
+
"an unexpected response."
|
|
565
|
+
)
|
|
566
|
+
1
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
def self.domains_remove(arguments)
|
|
570
|
+
unless Config.logged_in?
|
|
571
|
+
warn "You are not logged in."
|
|
572
|
+
warn "Run `railsdocks login` first."
|
|
573
|
+
return 1
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
hostname =
|
|
577
|
+
arguments.shift.to_s.strip
|
|
578
|
+
|
|
579
|
+
if hostname.empty? ||
|
|
580
|
+
!arguments.empty?
|
|
581
|
+
warn(
|
|
582
|
+
"Usage: railsdocks domains:remove DOMAIN"
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
return 1
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
response =
|
|
589
|
+
Client.new.remove_domain(
|
|
590
|
+
hostname: hostname,
|
|
591
|
+
token: Config.token
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
domain =
|
|
595
|
+
response.fetch("domain")
|
|
596
|
+
|
|
597
|
+
puts(
|
|
598
|
+
"Removed #{domain.fetch("hostname")} " \
|
|
599
|
+
"from #{domain.fetch("app")}."
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
0
|
|
603
|
+
rescue Client::Error => e
|
|
604
|
+
warn "Could not remove domain: #{e.message}"
|
|
605
|
+
1
|
|
606
|
+
rescue KeyError
|
|
607
|
+
warn(
|
|
608
|
+
"Could not remove domain: RailsDocks returned " \
|
|
609
|
+
"an unexpected response."
|
|
610
|
+
)
|
|
611
|
+
1
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
def self.inside_git_repository?
|
|
615
|
+
_output,
|
|
616
|
+
_error,
|
|
617
|
+
status =
|
|
618
|
+
Open3.capture3(
|
|
619
|
+
"git",
|
|
620
|
+
"rev-parse",
|
|
621
|
+
"--is-inside-work-tree"
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
status.success?
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
def self.current_app_slug
|
|
628
|
+
return nil unless inside_git_repository?
|
|
629
|
+
|
|
630
|
+
output,
|
|
631
|
+
_error,
|
|
632
|
+
status =
|
|
633
|
+
Open3.capture3(
|
|
634
|
+
"git",
|
|
635
|
+
"remote",
|
|
636
|
+
"get-url",
|
|
637
|
+
"railsdocks"
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
return nil unless status.success?
|
|
641
|
+
|
|
642
|
+
remote_url =
|
|
643
|
+
output.strip
|
|
644
|
+
|
|
645
|
+
match =
|
|
646
|
+
remote_url.match(
|
|
647
|
+
%r{
|
|
648
|
+
\A
|
|
649
|
+
git@deploy\.railsdocks\.com:
|
|
650
|
+
(?<slug>
|
|
651
|
+
[a-z0-9]
|
|
652
|
+
(?:[a-z0-9-]*[a-z0-9])?
|
|
653
|
+
)
|
|
654
|
+
\.git
|
|
655
|
+
\z
|
|
656
|
+
}x
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
match &&
|
|
660
|
+
match[:slug]
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
def self.add_git_remote(git_url)
|
|
664
|
+
_output,
|
|
665
|
+
_error,
|
|
666
|
+
status =
|
|
667
|
+
Open3.capture3(
|
|
668
|
+
"git",
|
|
669
|
+
"remote",
|
|
670
|
+
"get-url",
|
|
671
|
+
"railsdocks"
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
if status.success?
|
|
675
|
+
warn(
|
|
676
|
+
"Git remote `railsdocks` already exists."
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
return
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
_output,
|
|
683
|
+
error,
|
|
684
|
+
status =
|
|
685
|
+
Open3.capture3(
|
|
686
|
+
"git",
|
|
687
|
+
"remote",
|
|
688
|
+
"add",
|
|
689
|
+
"railsdocks",
|
|
690
|
+
git_url
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
unless status.success?
|
|
694
|
+
warn(
|
|
695
|
+
"Could not add Git remote: " \
|
|
696
|
+
"#{error.strip}"
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
return
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
puts "Git remote `railsdocks` added."
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
def self.help
|
|
706
|
+
puts <<~HELP
|
|
707
|
+
RailsDocks #{RailsDocks::VERSION}
|
|
708
|
+
|
|
709
|
+
Usage:
|
|
710
|
+
railsdocks COMMAND
|
|
711
|
+
|
|
712
|
+
Commands:
|
|
713
|
+
login
|
|
714
|
+
Log in to RailsDocks
|
|
715
|
+
|
|
716
|
+
setup
|
|
717
|
+
Configure RailsDocks SSH
|
|
718
|
+
|
|
719
|
+
apps:create APP_NAME [--no-db]
|
|
720
|
+
Create a RailsDocks application
|
|
721
|
+
|
|
722
|
+
--no-db
|
|
723
|
+
Create the application without PostgreSQL
|
|
724
|
+
|
|
725
|
+
apps:list
|
|
726
|
+
List RailsDocks applications
|
|
727
|
+
|
|
728
|
+
apps:destroy APP_NAME
|
|
729
|
+
Permanently destroy a RailsDocks application
|
|
730
|
+
|
|
731
|
+
db:migrate
|
|
732
|
+
Run Rails database migrations
|
|
733
|
+
|
|
734
|
+
db:rollback
|
|
735
|
+
Roll back the last application migration
|
|
736
|
+
|
|
737
|
+
domains:list
|
|
738
|
+
List all custom domains
|
|
739
|
+
|
|
740
|
+
domains:add DOMAIN [--app APP_NAME]
|
|
741
|
+
Add a custom domain
|
|
742
|
+
|
|
743
|
+
domains:remove DOMAIN
|
|
744
|
+
Remove a custom domain
|
|
745
|
+
|
|
746
|
+
version
|
|
747
|
+
Show the RailsDocks CLI version
|
|
748
|
+
|
|
749
|
+
help
|
|
750
|
+
Show this help
|
|
751
|
+
HELP
|
|
752
|
+
|
|
753
|
+
0
|
|
754
|
+
end
|
|
755
|
+
|
|
756
|
+
private_class_method \
|
|
757
|
+
:inside_git_repository?,
|
|
758
|
+
:current_app_slug,
|
|
759
|
+
:add_git_remote,
|
|
760
|
+
:help
|
|
761
|
+
end
|
|
762
|
+
end
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module RailsDocks
|
|
8
|
+
class Client
|
|
9
|
+
DEFAULT_API_URL = "https://api.railsdocks.com"
|
|
10
|
+
|
|
11
|
+
class Error < StandardError
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(
|
|
15
|
+
base_url: ENV.fetch(
|
|
16
|
+
"RAILSDOCKS_API_URL",
|
|
17
|
+
DEFAULT_API_URL
|
|
18
|
+
)
|
|
19
|
+
)
|
|
20
|
+
@base_url = base_url
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def login(email:, password:)
|
|
24
|
+
post(
|
|
25
|
+
"/api/v1/login",
|
|
26
|
+
{
|
|
27
|
+
email: email,
|
|
28
|
+
password: password
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def create_app(
|
|
34
|
+
name:,
|
|
35
|
+
token:,
|
|
36
|
+
no_db: false
|
|
37
|
+
)
|
|
38
|
+
post(
|
|
39
|
+
"/api/v1/apps",
|
|
40
|
+
{
|
|
41
|
+
name: name,
|
|
42
|
+
no_db: no_db
|
|
43
|
+
},
|
|
44
|
+
token: token
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def database_command(
|
|
49
|
+
slug:,
|
|
50
|
+
operation:,
|
|
51
|
+
token:
|
|
52
|
+
)
|
|
53
|
+
post(
|
|
54
|
+
"/api/v1/apps/#{slug}/db/#{operation}",
|
|
55
|
+
{},
|
|
56
|
+
token: token
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def apps(token:)
|
|
61
|
+
get(
|
|
62
|
+
"/api/v1/apps",
|
|
63
|
+
token: token
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def destroy_app(
|
|
68
|
+
slug:,
|
|
69
|
+
confirm:,
|
|
70
|
+
token:
|
|
71
|
+
)
|
|
72
|
+
encoded_slug =
|
|
73
|
+
URI.encode_www_form_component(
|
|
74
|
+
slug
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
delete(
|
|
78
|
+
"/api/v1/apps/#{encoded_slug}",
|
|
79
|
+
{
|
|
80
|
+
confirm: confirm
|
|
81
|
+
},
|
|
82
|
+
token: token
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def domains(token:)
|
|
87
|
+
get(
|
|
88
|
+
"/api/v1/domains",
|
|
89
|
+
token: token
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def add_domain(
|
|
94
|
+
hostname:,
|
|
95
|
+
app:,
|
|
96
|
+
token:
|
|
97
|
+
)
|
|
98
|
+
post(
|
|
99
|
+
"/api/v1/domains",
|
|
100
|
+
{
|
|
101
|
+
hostname: hostname,
|
|
102
|
+
app: app
|
|
103
|
+
},
|
|
104
|
+
token: token
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def remove_domain(
|
|
109
|
+
hostname:,
|
|
110
|
+
token:
|
|
111
|
+
)
|
|
112
|
+
delete(
|
|
113
|
+
"/api/v1/domains",
|
|
114
|
+
{
|
|
115
|
+
hostname: hostname
|
|
116
|
+
},
|
|
117
|
+
token: token
|
|
118
|
+
)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
attr_reader :base_url
|
|
124
|
+
|
|
125
|
+
def get(path, token: nil)
|
|
126
|
+
request(
|
|
127
|
+
Net::HTTP::Get,
|
|
128
|
+
path,
|
|
129
|
+
token: token
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def post(
|
|
134
|
+
path,
|
|
135
|
+
body,
|
|
136
|
+
token: nil
|
|
137
|
+
)
|
|
138
|
+
request(
|
|
139
|
+
Net::HTTP::Post,
|
|
140
|
+
path,
|
|
141
|
+
body: body,
|
|
142
|
+
token: token
|
|
143
|
+
)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def delete(
|
|
147
|
+
path,
|
|
148
|
+
body,
|
|
149
|
+
token: nil
|
|
150
|
+
)
|
|
151
|
+
request(
|
|
152
|
+
Net::HTTP::Delete,
|
|
153
|
+
path,
|
|
154
|
+
body: body,
|
|
155
|
+
token: token
|
|
156
|
+
)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def request(
|
|
160
|
+
request_class,
|
|
161
|
+
path,
|
|
162
|
+
body: nil,
|
|
163
|
+
token: nil
|
|
164
|
+
)
|
|
165
|
+
uri =
|
|
166
|
+
URI.join(
|
|
167
|
+
base_url,
|
|
168
|
+
path
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
http_request =
|
|
172
|
+
request_class.new(uri)
|
|
173
|
+
|
|
174
|
+
http_request["Accept"] =
|
|
175
|
+
"application/json"
|
|
176
|
+
|
|
177
|
+
if body
|
|
178
|
+
http_request["Content-Type"] =
|
|
179
|
+
"application/json"
|
|
180
|
+
|
|
181
|
+
http_request.body =
|
|
182
|
+
JSON.generate(body)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
if token
|
|
186
|
+
http_request["Authorization"] =
|
|
187
|
+
"Bearer #{token}"
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
response =
|
|
191
|
+
Net::HTTP.start(
|
|
192
|
+
uri.hostname,
|
|
193
|
+
uri.port,
|
|
194
|
+
use_ssl: uri.scheme == "https"
|
|
195
|
+
) do |http|
|
|
196
|
+
http.request(http_request)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
parsed_body =
|
|
200
|
+
if response.body.to_s.strip.empty?
|
|
201
|
+
{}
|
|
202
|
+
else
|
|
203
|
+
JSON.parse(response.body)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
if response.is_a?(
|
|
207
|
+
Net::HTTPSuccess
|
|
208
|
+
)
|
|
209
|
+
return parsed_body
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
message =
|
|
213
|
+
if parsed_body["error"]
|
|
214
|
+
parsed_body["error"]
|
|
215
|
+
elsif parsed_body["errors"]
|
|
216
|
+
Array(
|
|
217
|
+
parsed_body["errors"]
|
|
218
|
+
).join(", ")
|
|
219
|
+
else
|
|
220
|
+
"RailsDocks API request failed"
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
raise Error,
|
|
224
|
+
message
|
|
225
|
+
rescue Errno::ECONNREFUSED
|
|
226
|
+
raise Error,
|
|
227
|
+
"Could not connect to RailsDocks at #{base_url}"
|
|
228
|
+
rescue JSON::ParserError
|
|
229
|
+
raise Error,
|
|
230
|
+
"RailsDocks returned an invalid response"
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module RailsDocks
|
|
7
|
+
class Config
|
|
8
|
+
DIRECTORY = File.expand_path("~/.config/railsdocks")
|
|
9
|
+
FILE_PATH = File.join(DIRECTORY, "config.json")
|
|
10
|
+
|
|
11
|
+
def self.save_login(email:, token:)
|
|
12
|
+
FileUtils.mkdir_p(DIRECTORY)
|
|
13
|
+
|
|
14
|
+
File.write(
|
|
15
|
+
FILE_PATH,
|
|
16
|
+
JSON.pretty_generate(
|
|
17
|
+
email: email,
|
|
18
|
+
token: token
|
|
19
|
+
)
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
File.chmod(0o600, FILE_PATH)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.token
|
|
26
|
+
data.fetch("token")
|
|
27
|
+
rescue Errno::ENOENT, KeyError, JSON::ParserError
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.email
|
|
32
|
+
data.fetch("email")
|
|
33
|
+
rescue Errno::ENOENT, KeyError, JSON::ParserError
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.logged_in?
|
|
38
|
+
token.to_s != ""
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.data
|
|
42
|
+
JSON.parse(File.read(FILE_PATH))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private_class_method :data
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module RailsDocks
|
|
6
|
+
class SSHConfig
|
|
7
|
+
class Error < StandardError
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
SSH_DIRECTORY = File.expand_path("~/.ssh")
|
|
11
|
+
CONFIG_PATH = File.join(SSH_DIRECTORY, "config")
|
|
12
|
+
IDENTITY_PATH = File.join(SSH_DIRECTORY, "id_ed25519_railsdocks")
|
|
13
|
+
|
|
14
|
+
BEGIN_MARKER = "# BEGIN RailsDocks"
|
|
15
|
+
END_MARKER = "# END RailsDocks"
|
|
16
|
+
|
|
17
|
+
def self.configure
|
|
18
|
+
new.configure
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def configure
|
|
22
|
+
validate_identity!
|
|
23
|
+
prepare_directory!
|
|
24
|
+
write_config!
|
|
25
|
+
|
|
26
|
+
true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def validate_identity!
|
|
32
|
+
return if File.file?(IDENTITY_PATH)
|
|
33
|
+
|
|
34
|
+
raise Error,
|
|
35
|
+
"RailsDocks SSH key not found at #{IDENTITY_PATH}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def prepare_directory!
|
|
39
|
+
FileUtils.mkdir_p(SSH_DIRECTORY)
|
|
40
|
+
File.chmod(0o700, SSH_DIRECTORY)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def write_config!
|
|
44
|
+
existing =
|
|
45
|
+
if File.exist?(CONFIG_PATH)
|
|
46
|
+
File.read(CONFIG_PATH)
|
|
47
|
+
else
|
|
48
|
+
""
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
cleaned = remove_managed_block(existing)
|
|
52
|
+
|
|
53
|
+
content = ssh_block
|
|
54
|
+
|
|
55
|
+
unless cleaned.strip.empty?
|
|
56
|
+
content = "#{ssh_block}\n#{cleaned.lstrip}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
File.write(CONFIG_PATH, content)
|
|
60
|
+
File.chmod(0o600, CONFIG_PATH)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def remove_managed_block(content)
|
|
64
|
+
pattern = /
|
|
65
|
+
^\Q#{BEGIN_MARKER}\E\n
|
|
66
|
+
.*?
|
|
67
|
+
^\Q#{END_MARKER}\E\n?
|
|
68
|
+
/mx
|
|
69
|
+
|
|
70
|
+
content.gsub(pattern, "")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def ssh_block
|
|
74
|
+
<<~CONFIG
|
|
75
|
+
#{BEGIN_MARKER}
|
|
76
|
+
Host deploy.railsdocks.com
|
|
77
|
+
HostName deploy.railsdocks.com
|
|
78
|
+
User git
|
|
79
|
+
IdentityFile #{IDENTITY_PATH}
|
|
80
|
+
IdentitiesOnly yes
|
|
81
|
+
#{END_MARKER}
|
|
82
|
+
CONFIG
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
data/lib/railsdocks.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: railsdocks
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- John Nelson
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: json
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.0'
|
|
26
|
+
description: Deploy and manage Ruby on Rails applications on RailsDocks.
|
|
27
|
+
executables:
|
|
28
|
+
- railsdocks
|
|
29
|
+
extensions: []
|
|
30
|
+
extra_rdoc_files: []
|
|
31
|
+
files:
|
|
32
|
+
- exe/railsdocks
|
|
33
|
+
- lib/railsdocks.rb
|
|
34
|
+
- lib/railsdocks/cli.rb
|
|
35
|
+
- lib/railsdocks/client.rb
|
|
36
|
+
- lib/railsdocks/config.rb
|
|
37
|
+
- lib/railsdocks/ssh_config.rb
|
|
38
|
+
- lib/railsdocks/version.rb
|
|
39
|
+
homepage: https://railsdocks.com
|
|
40
|
+
licenses:
|
|
41
|
+
- MIT
|
|
42
|
+
metadata: {}
|
|
43
|
+
rdoc_options: []
|
|
44
|
+
require_paths:
|
|
45
|
+
- lib
|
|
46
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
47
|
+
requirements:
|
|
48
|
+
- - ">="
|
|
49
|
+
- !ruby/object:Gem::Version
|
|
50
|
+
version: '3.2'
|
|
51
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
52
|
+
requirements:
|
|
53
|
+
- - ">="
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: '0'
|
|
56
|
+
requirements: []
|
|
57
|
+
rubygems_version: 4.0.16
|
|
58
|
+
specification_version: 4
|
|
59
|
+
summary: Command-line interface for RailsDocks
|
|
60
|
+
test_files: []
|