govuk-connect 0.0.2 → 0.3.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 155ea20492095add3ae9e4680886c07012bcd01f41bdebfbe6cb43ad4b4b9cdc
4
- data.tar.gz: 902527a0b134cfebb2d7e85c0c49426f3834d2dd8df7c5e1899ba97249947f03
3
+ metadata.gz: 3ab00b53cb2051839c1904260cab6b947201e9a14ef150ccc68bed765ab5faed
4
+ data.tar.gz: aaf2f9d137b13b5fe11157a1dea809f6ec0ac8614c6139a7a1526fedf7d62c49
5
5
  SHA512:
6
- metadata.gz: 0cd41a07139275d3f1482863a23dad9f86a3cfe9526eb1291841117da08bb7d5491a5242a39bc2b7b8bf88b6f4b7dfc5daab0a0d1c99d2450abb864c0d554173
7
- data.tar.gz: 263cbc97edcabb670d577cf9a8dd53a70908b44066ca717395a4b55a15f730232121f8cccb1403e09db2e8bc410a2efd167bf473f9d5e83b5428f101ba3b1ed9
6
+ metadata.gz: 8e25fbc7b08900e9ffac04cc1a8491422c73179946e273bfe16e7754dfefa97d71741e3931bd4eb90d0c65fdf351ffb297aff83a604fadfeb95a76f33b35c36a
7
+ data.tar.gz: cb727b8cfa99fb47935358e7de62457d3ce65c78db64411b8a9d8f32dc21bae988b0a561723a87f4fb5e348d4871c554c934d54feb74954f2442cd65856c6e59
@@ -5,4 +5,4 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
5
 
6
6
  require 'govuk_connect'
7
7
 
8
- main
8
+ GovukConnect.main(ARGV)
@@ -1,1060 +1,7 @@
1
- #!/usr/bin/env ruby
1
+ require "govuk_connect/cli"
2
2
 
3
- #
4
- # This script aims to make it easy to connect to various parts of the GOV.UK
5
- # infrastructure. It aims to provide a consistent way to do this, even across
6
- # different hosting providers.
7
- #
8
- #
9
- # Usage: govuk-connect TYPE TARGET [options]
10
- # -e, --environment ENVIRONMENT Select which environment to connect to
11
- # --hosting-and-environment-from-alert-url URL
12
- # Select which environment to connect to based on the URL provided.
13
- # -p, --port-forward SERVICE Connect to a remote port
14
- # -v, --verbose Enable more detailed logging
15
- # -h, --help Prints usage information and examples
16
- #
17
- # CONNECTION TYPES
18
- # app-console
19
- # Launch a console for an application. For example, a rails console when connecting to a Rails application.
20
- # app-dbconsole
21
- # Launch a console for the database for an application.
22
- # rabbitmq
23
- # Setup port forwarding to the RabbitMQ admin interface.
24
- # sidekiq-monitoring
25
- # Setup port forwarding to the Sidekiq Monitoring application.
26
- # ssh
27
- # Connect to a machine through SSH.
28
- #
29
- # MACHINE TARGET
30
- # The ssh, rabbitmq and sidekiq-monitoring connection types target
31
- # machines.
32
- #
33
- # The machine can be specified by name, for example:
34
- #
35
- # govuk-connect ssh -e integration backend
36
- #
37
- # If the hosting provider is ambiguous, you'll need to specify it prior
38
- # to the name, for example:
39
- #
40
- # govuk-connect ssh -e staging aws/backend
41
- #
42
- # If you want to connect to a specific machine, you can specify a number
43
- # after the name, for example:
44
- #
45
- # govuk-connect ssh -e integration backend:2
46
- #
47
- # APPLICATION TARGET
48
- # The app-console and app-dbconsole connection types target
49
- # applications.
50
- #
51
- # The application is specified by name, for example:
52
- #
53
- # govuk-connect app-console -e integration publishing-api
54
- #
55
- # If the node class is ambiguous, you'll need to specify it prior to
56
- # the name, for example:
57
- #
58
- # govuk-connect app-console -e integration whitehall_backend/whitehall
59
- #
60
- # If you want to connect to a specific machine, you can specify a
61
- # number after the name, for example:
62
- #
63
- # govuk-connect app-console -e integration publishing-api:2
64
- #
65
- # EXAMPLES
66
- # govuk-connect ssh --environment integration backend
67
- #
68
- # govuk-connect app-console --environment staging publishing-api
69
- #
70
- # govuk-connect app-dbconsole -e integration whitehall_backend/whitehall
71
- #
72
- # govuk-connect rabbitmq -e staging aws/rabbitmq
73
- #
74
- # govuk-connect sidekiq-monitoring -e integration
75
-
76
-
77
- require 'uri'
78
- require 'yaml'
79
- require 'open3'
80
- require 'socket'
81
- require 'timeout'
82
- require 'optparse'
83
- require 'govuk_connect/version'
84
-
85
- def bold(string)
86
- "\e[1m#{string}\e[0m"
87
- end
88
-
89
- USAGE_BANNER = 'Usage: govuk-connect TYPE TARGET [options]'
90
-
91
- EXAMPLES = <<EXAMPLES
92
- govuk-connect ssh --environment integration backend
93
-
94
- govuk-connect app-console --environment staging publishing-api
95
-
96
- govuk-connect app-dbconsole -e integration whitehall_backend/whitehall
97
-
98
- govuk-connect rabbitmq -e staging aws/rabbitmq
99
-
100
- govuk-connect sidekiq-monitoring -e integration
101
- EXAMPLES
102
-
103
- MACHINE_TARGET_DESCRIPTION = <<DOCS
104
- The ssh, rabbitmq and sidekiq-monitoring connection types target
105
- machines.
106
-
107
- The machine can be specified by name, for example:
108
-
109
- govuk-connect ssh -e integration #{bold('backend')}
110
-
111
- If the hosting provider is ambiguous, you'll need to specify it prior
112
- to the name, for example:
113
-
114
- govuk-connect ssh -e staging #{bold('aws/')}backend
115
-
116
- If you want to connect to a specific machine, you can specify a number
117
- after the name, for example:
118
-
119
- govuk-connect ssh -e integration backend#{bold(':2')}
120
- DOCS
121
-
122
- APP_TARGET_DESCRIPTION = <<DOCS
123
- The app-console and app-dbconsole connection types target
124
- applications.
125
-
126
- The application is specified by name, for example:
127
-
128
- govuk-connect app-console -e integration #{bold('publishing-api')}
129
-
130
- If the node class is ambiguous, you'll need to specify it prior to
131
- the name, for example:
132
-
133
- govuk-connect app-console -e integration #{bold('whitehall_backend/')}whitehall
134
-
135
- If you want to connect to a specific machine, you can specify a
136
- number after the name, for example:
137
-
138
- govuk-connect app-console -e integration publishing-api#{bold(':2')}
139
- DOCS
140
-
141
- CONNECTION_TYPE_DESCRIPTIONS = {
142
- 'ssh' => 'Connect to a machine through SSH.',
143
- 'app-console' => 'Launch a console for an application. For example, a rails console when connecting to a Rails application.',
144
- 'app-dbconsole' => 'Launch a console for the database for an application.',
145
- 'rabbitmq' => 'Setup port forwarding to the RabbitMQ admin interface.',
146
- 'sidekiq-monitoring' => 'Setup port forwarding to the Sidekiq Monitoring application.'
147
- }
148
-
149
- RABBITMQ_PORT = 15672
150
- SIDEKIQ_MONITORING_PORT = 3211
151
-
152
- JUMPBOXES = {
153
- ci: {
154
- carrenza: 'ci-jumpbox.integration.publishing.service.gov.uk',
155
- },
156
- integration: {
157
- aws: 'jumpbox.integration.publishing.service.gov.uk',
158
- },
159
- staging: {
160
- carrenza: 'jumpbox.staging.publishing.service.gov.uk',
161
- aws: 'jumpbox.staging.govuk.digital',
162
- },
163
- production: {
164
- carrenza: 'jumpbox.publishing.service.gov.uk',
165
- aws: 'jumpbox.production.govuk.digital',
166
- }
167
- }
168
-
169
- def log(message)
170
- STDERR.puts message if $verbose
171
- end
172
-
173
- def newline
174
- STDERR.puts
175
- end
176
-
177
- def info(message)
178
- STDERR.puts message
179
- end
180
-
181
- def error(message)
182
- STDERR.puts "\e[1m\e[31m#{message}\e[0m"
183
- end
184
-
185
- def print_ssh_username_configuration_help
186
- info "The SSH username used was: #{bold(ssh_username)}"
187
- info "Check this is correct, and if it isn't, set the `USER` environment variable to the correct username."
188
- end
189
-
190
- # From Rosetta Code: https://rosettacode.org/wiki/Levenshtein_distance#Ruby
191
- def levenshtein_distance(a, b)
192
- a, b = a.downcase, b.downcase
193
- costs = Array(0..b.length) # i == 0
194
- (1..a.length).each do |i|
195
- costs[0], nw = i, i - 1 # j == 0; nw is lev(i-1, j)
196
- (1..b.length).each do |j|
197
- costs[j], nw = [
198
- costs[j] + 1, costs[j-1] + 1,
199
- a[i-1] == b[j-1] ? nw : nw + 1
200
- ].min, costs[j]
201
- end
202
- end
203
- costs[b.length]
204
- end
205
-
206
- def strings_similar_to(target, strings)
207
- strings.select do |s|
208
- levenshtein_distance(s, target) <= 3 # No specific reasoning for this value
209
- end
210
- end
211
-
212
- def check_ruby_version_greater_than(required_major:, required_minor:)
213
- major, minor = RUBY_VERSION.split '.'
214
-
215
- insufficient_version = (
216
- major.to_i < required_major || (
217
- major.to_i == required_major &&
218
- minor.to_i < required_minor
219
- )
220
- )
221
-
222
- if insufficient_version
223
- error "insufficient Ruby version: #{RUBY_VERSION}"
224
- error "must be at least #{required_major}.#{required_minor}"
225
-
226
- exit 1
227
- end
228
- end
229
-
230
- def port_free?(port)
231
- # No idea how well this works, but it's hopefully better than nothing
232
-
233
- log "debug: checking if port #{port} is free"
234
- Socket.tcp('127.0.0.1', port, connect_timeout: 0.1) {}
235
- false
236
- rescue Errno::ETIMEDOUT
237
- log "debug: port #{port} doesn't seem to be free"
238
- false
239
- rescue Errno::ECONNREFUSED
240
- log "debug: port #{port} is free"
241
- true
242
- end
243
-
244
- def random_free_port
245
- tries = 0
246
-
247
- while tries <= 10
248
- port = rand(32768...61000)
249
-
250
- return port if port_free? port
251
-
252
- tries += 1
253
- end
254
-
255
- raise "couldn't find open port"
256
- end
257
-
258
- def hosting_providers
259
- JUMPBOXES
260
- .map { |env, jumpboxes| jumpboxes.keys }
261
- .flatten
262
- .uniq
263
- end
264
-
265
- def jumpbox_for_environment_and_hosting(environment, hosting)
266
- raise 'missing environment' unless environment
267
- raise 'missing hosting' unless hosting
268
-
269
- jumpbox = JUMPBOXES[environment][hosting]
270
-
271
- unless jumpbox
272
- error "error: couldn't determine jumpbox for #{hosting}/#{environment}"
273
- exit 1
274
- end
275
-
276
- jumpbox
277
- end
278
-
279
- def single_hosting_provider_for_environment(environment)
280
- jumpboxes = JUMPBOXES[environment]
281
-
282
- if jumpboxes.size == 1
283
- jumpboxes.keys[0]
284
- else
285
- false
286
- end
287
- end
288
-
289
- def config_file
290
- @config_file ||= begin
291
- directory = ENV.fetch('XDG_CONFIG_HOME', "#{Dir.home}/.config")
292
-
293
- File.join(directory, 'config.yaml')
294
- end
295
- end
296
-
297
- def ssh_username
298
- @ssh_username ||= begin
299
- if File.exist? config_file
300
- config_ssh_username = YAML::load_file(config_file)['ssh_username']
301
- end
302
-
303
- config_ssh_username or ENV['USER']
304
- end
305
- end
306
-
307
- def ssh_identity_file
308
- @ssh_identity_file ||= begin
309
- YAML::load_file(config_file)['ssh_identity_file'] if File.exist? config_file
310
- end
311
- end
312
-
313
- def ssh_identity_arguments
314
- if ssh_identity_file
315
- ['-i', ssh_identity_file]
316
- else
317
- []
318
- end
319
- end
320
-
321
- def user_at_host(user, host)
322
- "#{user}@#{host}"
323
- end
324
-
325
- def govuk_node_list_classes(environment, hosting)
326
- log "debug: looking up classes in #{hosting}/#{environment}"
327
- command = [
328
- 'ssh',
329
- '-o', 'ConnectTimeout=2', # Show a failure quickly
330
- *ssh_identity_arguments,
331
- user_at_host(
332
- ssh_username,
333
- jumpbox_for_environment_and_hosting(environment, hosting)
334
- ),
335
- "govuk_node_list --classes",
336
- ].join(' ')
337
-
338
- log "debug: running command: #{command}"
339
- output, status = Open3.capture2(command)
340
-
341
- unless status.success?
342
- error "\nerror: command failed: #{command}"
343
- newline
344
- print_ssh_username_configuration_help
345
- exit 1
346
- end
347
-
348
- classes = output.split("\n").sort
349
-
350
- log "debug: classes:"
351
- classes.each { |c| log " - #{c}" }
352
-
353
- classes
354
- end
355
-
356
- def get_domains_for_node_class(target, environment, hosting, ssh_username)
357
- command = [
358
- 'ssh',
359
- '-o', 'ConnectTimeout=2', # Show a failure quickly
360
- *ssh_identity_arguments,
361
- user_at_host(
362
- ssh_username,
363
- jumpbox_for_environment_and_hosting(environment, hosting)
364
- ),
365
- "govuk_node_list -c #{target}",
366
- ].join(' ')
367
-
368
- output, status = Open3.capture2(command)
369
-
370
- unless status.success?
371
- error "error: command failed: #{command}"
372
- newline
373
- print_ssh_username_configuration_help
374
- exit 1
375
- end
376
-
377
- output.split("\n").sort
378
- end
379
-
380
- def govuk_directory
381
- File.join(ENV['HOME'], 'govuk')
382
- end
383
-
384
- def govuk_puppet_node_class_data(environment, hosting)
385
- log "debug: fetching govuk-puppet node class data for #{hosting} #{environment}"
386
-
387
- local_hieradata_root = File.join(
388
- govuk_directory,
389
- 'govuk-puppet',
390
- {
391
- carrenza: 'hieradata',
392
- aws: 'hieradata_aws'
393
- }[hosting]
394
- )
395
-
396
- hieradata_file = File.join(local_hieradata_root, "#{environment}.yaml")
397
- log "debug: reading #{hieradata_file}"
398
-
399
- environment_specific_hieradata = YAML::load_file(hieradata_file)
400
-
401
- if environment_specific_hieradata['node_class']
402
- environment_specific_hieradata['node_class']
403
- else
404
- common_hieradata = YAML::load_file(
405
- File.join(local_hieradata_root, 'common.yaml')
406
- )
407
-
408
- common_hieradata['node_class']
409
- end
410
- end
411
-
412
- def node_classes_for_environment_and_hosting(environment, hosting)
413
- govuk_puppet_node_class_data(
414
- environment,
415
- hosting
416
- ).map do |node_class, _data|
417
- node_class
418
- end
419
- end
420
-
421
- def application_names_from_node_class_data(environment, hosting)
422
- node_class_data = govuk_puppet_node_class_data(
423
- environment,
424
- hosting
425
- )
426
-
427
- all_names = node_class_data.flat_map do |node_class, data|
428
- data['apps']
429
- end
430
-
431
- all_names.sort.uniq
432
- end
433
-
434
- def node_class_for_app(app_name, environment, hosting)
435
- log "debug: finding node class for #{app_name} in #{hosting} #{environment}"
436
-
437
- node_class_data = govuk_puppet_node_class_data(
438
- environment,
439
- hosting
440
- )
441
-
442
- app_lookup_hash = {}
443
- node_class_data.each do |node_class, data|
444
- data['apps'].each do |app|
445
- if app_lookup_hash.key? app
446
- app_lookup_hash[app] += [node_class]
447
- else
448
- app_lookup_hash[app] = [node_class]
449
- end
450
- end
451
- end
452
-
453
- node_classes = app_lookup_hash[app_name]
454
-
455
- return if node_classes.nil?
456
-
457
- if node_classes.length > 1
458
- error "error: ambiguous node class for #{app_name} in #{environment}"
459
- newline
460
- info "specify the node class and application mame, for example: "
461
- node_classes.each do |node_class|
462
- info "\n govuk-connect app-console -e #{environment} #{node_class}/#{app_name}"
463
- end
464
- newline
465
-
466
- exit 1
467
- else
468
- node_class = node_classes.first
469
- end
470
-
471
- log "debug: node class: #{node_class}"
472
-
473
- node_class
474
- end
475
-
476
- def hosting_for_target_and_environment(target, environment)
477
- hosting = single_hosting_provider_for_environment(
478
- environment
479
- )
480
-
481
- unless hosting
482
- hosting, name, _number = parse_hosting_name_and_number(target)
483
-
484
- unless hosting
485
- hosting = hosting_for_node_type(name, environment)
486
- end
487
- end
488
-
489
- hosting
490
- end
491
-
492
- def hosting_for_node_type(node_type, environment)
493
- log "debug: Looking up hosting for node_type: #{node_type}"
494
- hosting = single_hosting_provider_for_environment(environment)
495
-
496
- return hosting if hosting
497
-
498
- aws_node_types = govuk_node_list_classes(environment, :aws)
499
- carrenza_node_types = govuk_node_list_classes(environment, :carrenza)
500
-
501
- if (
502
- aws_node_types.include?(node_type) &&
503
- carrenza_node_types.include?(node_type)
504
- )
505
- error "error: ambiguous hosting for #{node_type} in #{environment}"
506
- newline
507
- info "specify the hosting provider and node type, for example: "
508
- hosting_providers.each do |hosting_provider|
509
- info "\n govuk-connect ssh #{bold(hosting_provider)}/#{node_type}"
510
- end
511
- info "\n"
512
-
513
- exit 1
514
- elsif aws_node_types.include?(node_type)
515
- :aws
516
- elsif carrenza_node_types.include?(node_type)
517
- :carrenza
518
- else
519
- error "error: couldn't find #{node_type} in #{environment}"
520
-
521
- all_node_types = (aws_node_types + carrenza_node_types).uniq.sort
522
- similar_node_types = strings_similar_to(node_type, all_node_types)
523
-
524
- if similar_node_types.any?
525
- info "\ndid you mean:"
526
- similar_node_types.each { |s| info " - #{s}" }
527
- else
528
- info "\nall node types:"
529
- all_node_types.each { |s| info " - #{s}" }
530
- end
531
-
532
- exit 1
533
- end
534
- end
535
-
536
- def hosting_for_app(app_name, environment)
537
- log "debug: finding hosting for #{app_name} in #{environment}"
538
-
539
- hosting = single_hosting_provider_for_environment(environment)
540
-
541
- if hosting
542
- log "debug: this environment has a single hosting provider: #{hosting}"
543
- return hosting
544
- end
545
-
546
- aws_app_names = application_names_from_node_class_data(
547
- environment,
548
- :aws,
549
- )
550
-
551
- if aws_app_names.include? app_name
552
- log "debug: #{app_name} is hosted in AWS"
553
-
554
- return :aws
555
- end
556
-
557
- carrenza_app_names = application_names_from_node_class_data(
558
- environment,
559
- :carrenza,
560
- )
561
-
562
- if carrenza_app_names.include? app_name
563
- log "debug: #{app_name} is hosted in Carrenza"
564
-
565
- return :carrenza
566
- end
567
-
568
- error "error: unknown hosting value '#{hosting}' for #{app_name}"
569
- exit 1
570
- end
571
-
572
- def govuk_app_command(target, environment, command)
573
- node_class, app_name, number = parse_node_class_app_name_and_number(target)
574
-
575
- info "Connecting to the app #{command} for #{bold(app_name)},\
576
- in the #{bold(environment)} environment"
577
-
578
- hosting = hosting_for_app(app_name, environment)
579
-
580
- info "The relevant hosting provider is #{bold(hosting)}"
581
-
582
- unless node_class
583
- node_class = node_class_for_app(
584
- app_name,
585
- environment,
586
- hosting
587
- )
588
- end
589
-
590
- unless node_class
591
- error "error: application '#{app_name}' not found."
592
- newline
593
-
594
- application_names = application_names_from_node_class_data(
595
- environment,
596
- hosting
597
- )
598
-
599
- similar_application_names = strings_similar_to(app_name, application_names)
600
- if similar_application_names.any?
601
- info "did you mean:"
602
- similar_application_names.each { |s| info " - #{s}" }
603
- else
604
- info "all applications:"
605
- newline
606
- info " #{application_names.join(', ')}"
607
- newline
608
- end
609
-
610
- exit 1
611
- end
612
-
613
- info "The relevant node class is #{bold(node_class)}"
614
-
615
- ssh(
616
- {
617
- hosting: hosting,
618
- name: node_class,
619
- number: number
620
- },
621
- environment,
622
- command: "govuk_app_#{command} #{app_name}"
623
- )
624
- end
625
-
626
- def ssh(
627
- target,
628
- environment,
629
- command: false,
630
- port_forward: false,
631
- additional_arguments: []
632
- )
633
- log "debug: ssh to #{target} in #{environment}"
634
-
635
- # Split something like aws/backend:2 in to :aws, 'backend', 2
636
- hosting, name, number = parse_hosting_name_and_number(target)
637
-
638
- if name.end_with? '.internal'
639
- ssh_target = name
640
- hosting = :aws
641
- elsif name.end_with? '.gov.uk'
642
- ssh_target = name
643
- hosting = :carrenza
644
- else
645
- # The hosting might not have been provided, so check if necessary
646
- hosting ||= hosting_for_target_and_environment(target, environment)
647
-
648
- domains = get_domains_for_node_class(
649
- name,
650
- environment,
651
- hosting,
652
- ssh_username
653
- )
654
-
655
- if domains.length.zero?
656
- error "error: couldn't find #{name} in #{hosting}/#{environment}"
657
-
658
- node_types = govuk_node_list_classes(environment, hosting)
659
-
660
- similar_node_types = strings_similar_to(name, node_types)
661
-
662
- if similar_node_types.any?
663
- info "\ndid you mean:"
664
- similar_node_types.each { |s| info " - #{s}" }
665
- else
666
- info "\nall node types:"
667
- node_types.each { |s| info " - #{s}" }
668
- end
669
-
670
- exit 1
671
- elsif domains.length == 1
672
- ssh_target = domains.first
673
-
674
- info "There is #{bold('one machine')} to connect to"
675
- else
676
- n_machines = bold("#{domains.length} machines")
677
- info "There are #{n_machines} of this class"
678
-
679
- if number
680
- unless number > 0
681
- newline
682
- error "error: invalid machine number '#{number}', it must be > 0"
683
- exit 1
684
- end
685
-
686
- unless number <= domains.length
687
- newline
688
- error "error: cannot connect to machine number: #{number}"
689
- exit 1
690
- end
691
-
692
- ssh_target = domains[number - 1]
693
- info "Connecting to number #{number}"
694
- else
695
- ssh_target = domains.sample
696
- info "Connecting to a random machine (number #{domains.find_index(ssh_target) + 1})"
697
- end
698
- end
699
- end
700
-
701
- ssh_command = [
702
- 'ssh',
703
- *ssh_identity_arguments,
704
- '-J', user_at_host(
705
- ssh_username,
706
- jumpbox_for_environment_and_hosting(environment, hosting)
707
- ),
708
- user_at_host(
709
- ssh_username,
710
- ssh_target
711
- )
712
- ]
713
-
714
- if command
715
- ssh_command += [
716
- '-t', # Force tty allocation so that interactive commands work
717
- command
718
- ]
719
- elsif port_forward
720
- localhost_port = random_free_port
721
-
722
- ssh_command += [
723
- '-N',
724
- '-L', "#{localhost_port}:127.0.0.1:#{port_forward}"
725
- ]
726
-
727
- info "Port forwarding setup, access:\n\n http://127.0.0.1:#{localhost_port}/\n\n"
728
- end
729
-
730
- ssh_command += additional_arguments
731
-
732
- info "\n#{bold('Running command:')} #{ssh_command.join(' ')}\n\n"
733
-
734
- exec(*ssh_command)
735
- end
736
-
737
- def rabbitmq_root_password_command(hosting, environment)
738
- hieradata_directory = {
739
- aws: 'puppet_aws',
740
- carrenza: 'puppet'
741
- }[hosting]
742
-
743
- directory = File.join(
744
- govuk_directory,
745
- 'govuk-secrets',
746
- hieradata_directory
747
- )
748
-
749
- "cd #{directory} && rake eyaml:decrypt_value[#{environment},govuk_rabbitmq::root_password]"
750
- end
751
-
752
- def hosting_and_environment_from_url(url)
753
- uri = URI(url)
754
-
755
- host_to_hosting_and_environment = {
756
- 'ci-alert.integration.publishing.service.gov.uk' => %i[carrenza ci],
757
- 'alert.integration.publishing.service.gov.uk' => %i[aws integration],
758
- 'alert.staging.govuk.digital' => %i[aws staging],
759
- 'alert.blue.staging.govuk.digital' => %i[aws staging],
760
- 'alert.staging.publishing.service.gov.uk' => %i[carrenza staging],
761
- 'alert.production.govuk.digital' => %i[aws production],
762
- 'alert.blue.production.govuk.digital' => %i[aws production],
763
- 'alert.publishing.service.gov.uk' => %i[carrenza production],
764
- }
765
-
766
- unless host_to_hosting_and_environment.key? uri.host
767
- error "error: unknown hosting and environment for: #{uri.host}"
768
- exit 1
769
- end
770
-
771
- host_to_hosting_and_environment[uri.host]
772
- end
773
-
774
- def parse_options
775
- options = {}
776
-
777
- @option_parser = OptionParser.new do |opts|
778
- opts.banner = USAGE_BANNER
779
-
780
- opts.on(
781
- '-e',
782
- '--environment ENVIRONMENT',
783
- 'Select which environment to connect to'
784
- ) do |o|
785
- options[:environment] = o.to_sym
786
- end
787
- opts.on(
788
- '--hosting-and-environment-from-alert-url URL',
789
- 'Select which environment to connect to based on the URL provided.'
790
- ) do |o|
791
- hosting, environment = hosting_and_environment_from_url(o)
792
- options[:hosting] = hosting
793
- options[:environment] = environment
794
- end
795
- opts.on('-p', '--port-forward SERVICE', 'Connect to a remote port') do |o|
796
- options[:port_forward] = o
797
- end
798
- opts.on('-v', '--verbose', 'Enable more detailed logging') do
799
- $verbose = true
800
- end
801
-
802
- opts.on('-h', '--help', 'Prints usage information and examples') do
803
- info opts
804
- newline
805
- info bold('CONNECTION TYPES')
806
- TYPES.keys.each do |x|
807
- info " #{x}"
808
- description = CONNECTION_TYPE_DESCRIPTIONS[x]
809
- info " #{description}" if description
810
- end
811
- newline
812
- info bold('MACHINE TARGET')
813
- info MACHINE_TARGET_DESCRIPTION
814
- newline
815
- info bold('APPLICATION TARGET')
816
- info APP_TARGET_DESCRIPTION
817
- newline
818
- info bold('EXAMPLES')
819
- info EXAMPLES
820
- exit
821
- end
822
- opts.on('-V', '--version', 'Prints version information') do
823
- info "#{GovukConnect::VERSION}"
824
- exit
825
- end
826
- end
827
-
828
- @option_parser.parse!
829
-
830
- options
831
- end
832
-
833
- def parse_hosting_name_and_number(target)
834
- log "debug: parsing target: #{target}"
835
- if target.is_a? Hash
836
- return %i[hosting name number].map do |key|
837
- target[key]
838
- end
839
- end
840
-
841
- if target.include? '/'
842
- hosting, name_and_number = target.split '/'
843
-
844
- hosting = hosting.to_sym
845
-
846
- unless %i[carrenza aws].include? hosting
847
- error "error: unknown hosting provider: #{hosting}"
848
- newline
849
- info "available hosting providers are:"
850
- hosting_providers.each { |x| info " - #{x}" }
851
-
852
- exit 1
853
- end
854
- else
855
- name_and_number = target
856
- end
857
-
858
- if name_and_number.include? ':'
859
- name, number = name_and_number.split ':'
860
-
861
- number = number.to_i
862
- else
863
- name = name_and_number
864
- end
865
-
866
- log "debug: hosting: #{hosting.inspect}, name: #{name.inspect}, number: #{number.inspect}"
867
-
868
- return hosting, name, number
869
- end
870
-
871
- def parse_node_class_app_name_and_number(target)
872
- log "debug: parsing target: #{target}"
873
- if target.is_a? Hash
874
- return %i[node_class app_name number].map do |key|
875
- target[key]
876
- end
877
- end
878
-
879
- if target.include? '/'
880
- node_class, app_name_and_number = target.split '/'
881
- else
882
- app_name_and_number = target
883
- end
884
-
885
- if app_name_and_number.include? ':'
886
- app_name, number = name_and_number.split ':'
887
-
888
- number = number.to_i
889
- else
890
- app_name = app_name_and_number
891
- end
892
-
893
- log "debug: node_class: #{node_class.inspect}, app_name: #{app_name.inspect}, number: #{number.inspect}"
894
-
895
- return node_class, app_name, number
896
- end
897
-
898
- def check_for_target(target)
899
- unless target
900
- error "error: you must specify the target\n"
901
- STDERR.puts USAGE_BANNER
902
- STDERR.puts
903
- STDERR.puts EXAMPLES
904
- exit 1
905
- end
906
- end
907
-
908
- def check_for_additional_arguments(command, args)
909
- unless args.empty?
910
- error "error: #{command} doesn't support arguments: #{args}"
911
- exit 1
912
- end
913
- end
914
-
915
- TYPES = {
916
- 'app-console' => Proc.new do |target, environment, args, _options|
917
- check_for_target(target)
918
- check_for_additional_arguments('app-console', args)
919
- govuk_app_command(target, environment, 'console')
920
- end,
921
-
922
- 'app-dbconsole' => Proc.new do |target, environment, args, _options|
923
- check_for_target(target)
924
- check_for_additional_arguments('app-dbconsole', args)
925
- govuk_app_command(target, environment, 'dbconsole')
926
- end,
927
-
928
- 'rabbitmq' => Proc.new do |target, environment, args, options|
929
- check_for_additional_arguments('rabbitmq', args)
930
-
931
- target ||= 'rabbitmq'
932
-
933
- root_password_command = rabbitmq_root_password_command(
934
- hosting_for_target_and_environment(target, environment),
935
- environment
936
- )
937
-
938
- info "You'll need to login as the RabbitMQ #{bold('root')} user."
939
- info "Get the password from govuk-secrets, or example:\n\n"
940
- info " #{bold(root_password_command)}"
941
- newline
942
-
943
- ssh(
944
- target,
945
- environment,
946
- port_forward: RABBITMQ_PORT,
947
- )
948
- end,
949
-
950
- 'sidekiq-monitoring' => Proc.new do |target, environment, args, options|
951
- check_for_additional_arguments('sidekiq-monitoring', args)
952
- ssh(
953
- target || 'backend',
954
- environment,
955
- port_forward: SIDEKIQ_MONITORING_PORT,
956
- )
957
- end,
958
-
959
- 'ssh' => Proc.new do |target, environment, args, options|
960
- check_for_target(target)
961
-
962
- if options.key? :hosting
963
- hosting, name, number = parse_hosting_name_and_number(target)
964
- if hosting
965
- error "error: hosting specified twice"
966
- exit 1
967
- end
968
-
969
- target = {
970
- hosting: options[:hosting],
971
- name: name,
972
- number: number
973
- }
974
- end
975
-
976
- ssh(
977
- target,
978
- environment,
979
- port_forward: options[:port_forward],
980
- additional_arguments: args
981
- )
3
+ module GovukConnect
4
+ def self.main(argv)
5
+ CLI.new.main(argv)
982
6
  end
983
- }
984
-
985
- def main
986
- check_ruby_version_greater_than(required_major: 2, required_minor: 0)
987
-
988
- double_dash_index = ARGV.index '--'
989
- if double_dash_index
990
- # This is used in the case of passing extra options to ssh, the --
991
- # acts as a separator, so to avoid optparse interpreting those
992
- # options, split ARGV around -- before parsing the options
993
- rest = ARGV[double_dash_index + 1, ARGV.length]
994
- argv = ARGV[0, double_dash_index]
995
-
996
- ARGV.clear
997
- ARGV.concat argv
998
-
999
- options = parse_options
1000
-
1001
- type, target = ARGV
1002
- else
1003
- options = parse_options
1004
-
1005
- type, target, *rest = ARGV
1006
- end
1007
-
1008
- unless type
1009
- error "error: you must specify the connection type\n"
1010
-
1011
- STDERR.puts @option_parser.help
1012
-
1013
- STDERR.puts "\nValid connection types are:\n"
1014
- TYPES.keys.each do |x|
1015
- STDERR.puts " - #{x}"
1016
- end
1017
- STDERR.puts
1018
- STDERR.puts "Example commands:"
1019
- STDERR.puts EXAMPLES
1020
-
1021
- exit 1
1022
- end
1023
-
1024
- handler = TYPES[type]
1025
-
1026
- unless handler
1027
- error "error: unknown connection type: #{type}\n"
1028
-
1029
- STDERR.puts "Valid connection types are:\n"
1030
- TYPES.keys.each do |x|
1031
- STDERR.puts " - #{x}"
1032
- end
1033
- STDERR.puts
1034
- STDERR.puts "Example commands:"
1035
- STDERR.puts EXAMPLES
1036
-
1037
- exit 1
1038
- end
1039
-
1040
- environment = options[:environment]&.to_sym
1041
-
1042
- unless environment
1043
- error "error: you must specify the environment\n"
1044
- STDERR.puts @option_parser.help
1045
- exit 1
1046
- end
1047
-
1048
- unless JUMPBOXES.key? environment
1049
- error "error: unknown environment '#{environment}'"
1050
- newline
1051
- info "Valid environments are:"
1052
- JUMPBOXES.keys.each { |e| info " - #{e}" }
1053
- exit 1
1054
- end
1055
-
1056
- handler.call(target, environment, rest, options)
1057
- rescue Interrupt
1058
- # Handle SIGTERM without printing a stacktrace
1059
- exit 1
1060
7
  end