govuk-connect 0.0.2 → 0.1.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 +4 -4
- data/bin/govuk-connect +1 -1
- data/lib/govuk_connect/cli.rb +991 -0
- data/lib/govuk_connect/version.rb +1 -1
- data/lib/govuk_connect.rb +4 -1057
- metadata +62 -5
data/lib/govuk_connect.rb
CHANGED
@@ -1,1060 +1,7 @@
|
|
1
|
-
|
1
|
+
require "govuk_connect/cli"
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
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
|