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 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,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "railsdocks"
5
+
6
+ exit RailsDocks::CLI.start(ARGV)
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsDocks
4
+ VERSION = "1.0.0"
5
+ end
data/lib/railsdocks.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "railsdocks/version"
4
+ require_relative "railsdocks/client"
5
+ require_relative "railsdocks/config"
6
+ require_relative "railsdocks/ssh_config"
7
+ require_relative "railsdocks/cli"
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: []