@govuk-pay/cli 0.0.2 → 0.0.4

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.
Files changed (89) hide show
  1. package/package.json +1 -1
  2. package/resources/legacy-ruby-cli/.rspec +1 -0
  3. package/resources/legacy-ruby-cli/.rubocop.yml +26 -0
  4. package/resources/legacy-ruby-cli/.ruby-version +1 -0
  5. package/resources/legacy-ruby-cli/Gemfile +24 -0
  6. package/resources/legacy-ruby-cli/Gemfile.lock +1431 -0
  7. package/resources/legacy-ruby-cli/README.md +142 -0
  8. package/resources/legacy-ruby-cli/bin/pay +31 -0
  9. package/resources/legacy-ruby-cli/config/generate-secrets.yml +9 -0
  10. package/resources/legacy-ruby-cli/config/secrets.yml +581 -0
  11. package/resources/legacy-ruby-cli/config/service_secrets.yml +174 -0
  12. package/resources/legacy-ruby-cli/lib/pay_cli/aws/document.rb +23 -0
  13. package/resources/legacy-ruby-cli/lib/pay_cli/aws/services.rb +47 -0
  14. package/resources/legacy-ruby-cli/lib/pay_cli/aws/tokens.rb +161 -0
  15. package/resources/legacy-ruby-cli/lib/pay_cli/commands/aws.rb +51 -0
  16. package/resources/legacy-ruby-cli/lib/pay_cli/commands/browse.rb +31 -0
  17. package/resources/legacy-ruby-cli/lib/pay_cli/commands/doctor.rb +154 -0
  18. package/resources/legacy-ruby-cli/lib/pay_cli/commands/local/app_client.rb +216 -0
  19. package/resources/legacy-ruby-cli/lib/pay_cli/commands/local/config.rb +138 -0
  20. package/resources/legacy-ruby-cli/lib/pay_cli/commands/local/config.yaml +192 -0
  21. package/resources/legacy-ruby-cli/lib/pay_cli/commands/local/docker.rb +36 -0
  22. package/resources/legacy-ruby-cli/lib/pay_cli/commands/local/files/docker-compose.erb +270 -0
  23. package/resources/legacy-ruby-cli/lib/pay_cli/commands/local/files/end-to-end.erb +30 -0
  24. package/resources/legacy-ruby-cli/lib/pay_cli/commands/local/files/localstack/init-aws.sh +70 -0
  25. package/resources/legacy-ruby-cli/lib/pay_cli/commands/local/files/naxsi/readme.md +1 -0
  26. package/resources/legacy-ruby-cli/lib/pay_cli/commands/local/files/postgres/docker-entrypoint-initdb.d/make_payments_databases.sql +26 -0
  27. package/resources/legacy-ruby-cli/lib/pay_cli/commands/local/files/services/adminusers.env +49 -0
  28. package/resources/legacy-ruby-cli/lib/pay_cli/commands/local/files/services/cardid.env +2 -0
  29. package/resources/legacy-ruby-cli/lib/pay_cli/commands/local/files/services/connector.env +70 -0
  30. package/resources/legacy-ruby-cli/lib/pay_cli/commands/local/files/services/demo-service.env +10 -0
  31. package/resources/legacy-ruby-cli/lib/pay_cli/commands/local/files/services/frontend.env +12 -0
  32. package/resources/legacy-ruby-cli/lib/pay_cli/commands/local/files/services/java_app.env +1 -0
  33. package/resources/legacy-ruby-cli/lib/pay_cli/commands/local/files/services/ledger.env +7 -0
  34. package/resources/legacy-ruby-cli/lib/pay_cli/commands/local/files/services/products-ui.env +14 -0
  35. package/resources/legacy-ruby-cli/lib/pay_cli/commands/local/files/services/products.env +25 -0
  36. package/resources/legacy-ruby-cli/lib/pay_cli/commands/local/files/services/publicapi.env +13 -0
  37. package/resources/legacy-ruby-cli/lib/pay_cli/commands/local/files/services/publicauth.env +13 -0
  38. package/resources/legacy-ruby-cli/lib/pay_cli/commands/local/files/services/selfservice.env +21 -0
  39. package/resources/legacy-ruby-cli/lib/pay_cli/commands/local/files/services/ssl/certs/frontend-proxy.crt +18 -0
  40. package/resources/legacy-ruby-cli/lib/pay_cli/commands/local/files/services/ssl/certs/products-ui-proxy.crt +20 -0
  41. package/resources/legacy-ruby-cli/lib/pay_cli/commands/local/files/services/ssl/certs/selfservice-proxy.crt +20 -0
  42. package/resources/legacy-ruby-cli/lib/pay_cli/commands/local/files/services/ssl/certs/stubs-proxy.crt +18 -0
  43. package/resources/legacy-ruby-cli/lib/pay_cli/commands/local/files/services/ssl/keys/frontend-proxy.key +28 -0
  44. package/resources/legacy-ruby-cli/lib/pay_cli/commands/local/files/services/ssl/keys/products-ui-proxy.key +28 -0
  45. package/resources/legacy-ruby-cli/lib/pay_cli/commands/local/files/services/ssl/keys/selfservice-proxy.key +28 -0
  46. package/resources/legacy-ruby-cli/lib/pay_cli/commands/local/files/services/ssl/keys/stubs-proxy.key +28 -0
  47. package/resources/legacy-ruby-cli/lib/pay_cli/commands/local/files/services/ssl/make-selfsigned.sh +2 -0
  48. package/resources/legacy-ruby-cli/lib/pay_cli/commands/local/files/services/stubs.env +12 -0
  49. package/resources/legacy-ruby-cli/lib/pay_cli/commands/local/files/services/toolbox.env +5 -0
  50. package/resources/legacy-ruby-cli/lib/pay_cli/commands/local/files/services/webhooks.env +9 -0
  51. package/resources/legacy-ruby-cli/lib/pay_cli/commands/local/image_extractor.rb +20 -0
  52. package/resources/legacy-ruby-cli/lib/pay_cli/commands/local.rb +430 -0
  53. package/resources/legacy-ruby-cli/lib/pay_cli/commands/schema.rb +36 -0
  54. package/resources/legacy-ruby-cli/lib/pay_cli/commands/secrets.rb +114 -0
  55. package/resources/legacy-ruby-cli/lib/pay_cli/commands/ssm.rb +111 -0
  56. package/resources/legacy-ruby-cli/lib/pay_cli/commands/tf.rb +90 -0
  57. package/resources/legacy-ruby-cli/lib/pay_cli/commands/tunnel/services.yml +49 -0
  58. package/resources/legacy-ruby-cli/lib/pay_cli/config.rb +27 -0
  59. package/resources/legacy-ruby-cli/lib/pay_cli/ec2.rb +38 -0
  60. package/resources/legacy-ruby-cli/lib/pay_cli/entry_point.rb +52 -0
  61. package/resources/legacy-ruby-cli/lib/pay_cli/environment.rb +25 -0
  62. package/resources/legacy-ruby-cli/lib/pay_cli/logger.rb +3 -0
  63. package/resources/legacy-ruby-cli/lib/pay_cli/logs.rb +248 -0
  64. package/resources/legacy-ruby-cli/lib/pay_cli/naming.rb +44 -0
  65. package/resources/legacy-ruby-cli/lib/pay_cli/secrets.rb +276 -0
  66. package/resources/legacy-ruby-cli/lib/pay_cli/stop_yubico_authenticator.rb +10 -0
  67. package/resources/legacy-ruby-cli/lib/pay_cli/ykman_oath_credential_config.rb +70 -0
  68. package/resources/legacy-ruby-cli/lib/zeitwerk_setup.rb +5 -0
  69. package/resources/legacy-ruby-cli/package-lock.json +6 -0
  70. package/resources/legacy-ruby-cli/rds_access/connect.sh +149 -0
  71. package/resources/legacy-ruby-cli/spec/.rubocop.yml +2 -0
  72. package/resources/legacy-ruby-cli/spec/fixtures/dockerfile_examples/Dockerfile.complex +34 -0
  73. package/resources/legacy-ruby-cli/spec/fixtures/dockerfile_examples/Dockerfile.complex_differing_froms +33 -0
  74. package/resources/legacy-ruby-cli/spec/fixtures/dockerfile_examples/Dockerfile.no_from +3 -0
  75. package/resources/legacy-ruby-cli/spec/fixtures/dockerfile_examples/Dockerfile.simple +5 -0
  76. package/resources/legacy-ruby-cli/spec/fixtures/dockerfile_examples/Dockerfile.simple_no_tag +5 -0
  77. package/resources/legacy-ruby-cli/spec/fixtures/dockerfile_examples/Dockerfile.with_sha +5 -0
  78. package/resources/legacy-ruby-cli/spec/fixtures/dockerfile_examples/Dockerfile.with_sha_no_tag +5 -0
  79. package/resources/legacy-ruby-cli/spec/lib/pay_cli/commands/local/image_extractor_spec.rb +55 -0
  80. package/resources/legacy-ruby-cli/spec/naming_spec.rb +83 -0
  81. package/resources/legacy-ruby-cli/spec/spec_helper.rb +106 -0
  82. package/resources/legacy-ruby-cli/vulnerability_scan/.nvmrc +1 -0
  83. package/resources/legacy-ruby-cli/vulnerability_scan/generate_vulnerability_report.js +91 -0
  84. package/resources/legacy-ruby-cli/vulnerability_scan/reports/.gitkeep +0 -0
  85. package/resources/legacy-ruby-cli/vulnerability_scan/scan.sh +57 -0
  86. package/src/commands/browse.js +2 -2
  87. package/src/commands/legacy.js +5 -2
  88. package/src/core/constants.js +7 -10
  89. package/src/util/payCliExec.js +18 -1
@@ -0,0 +1,276 @@
1
+ require 'open3'
2
+ require 'pp'
3
+ require 'yaml'
4
+
5
+ require 'aws-sdk-ssm'
6
+ require 'tty-table'
7
+
8
+ module PayCLI::Secrets
9
+ SECRETS_FILE_PATH = File.join(PayCLI::Config::CONFIG_PATH, 'secrets.yml')
10
+ GENERATE_FILE_PATH = File.join(PayCLI::Config::CONFIG_PATH, 'generate-secrets.yml')
11
+
12
+ SECRETS_PROVIDERS = Proc.new do
13
+ # We should produce a mapping of env -> secret_name -> provider
14
+ mapping = {}
15
+
16
+ generate_secrets_info = YAML.load_file(GENERATE_FILE_PATH)
17
+
18
+ YAML.load_file(SECRETS_FILE_PATH).each do |provider, environments|
19
+ environments.map do |env, secrets|
20
+ generate_secrets_info.each do |service, service_generate_names|
21
+ mapping[env] ||= {}
22
+ service_generate_names.each do | generate_info |
23
+ mapping[env][service] ||= {}
24
+ mapping[env][service][generate_info.first] = {
25
+ provider: 'generate',
26
+ detail: generate_info.last
27
+ }
28
+ end
29
+ end
30
+ secrets.each do |service, service_secrets|
31
+ mapping[env] ||= {}
32
+
33
+ service_secrets.each do |secret_info|
34
+ mapping[env][service] ||= {}
35
+
36
+ if secret_info.class == Array
37
+ secret_name = secret_info.first
38
+
39
+ mapping[env][service][secret_name] = {
40
+ provider: provider,
41
+ detail: secret_info.last
42
+ }
43
+ else
44
+ mapping[env][service][secret_info] = {
45
+ provider: provider,
46
+ }
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ mapping
54
+ end.call
55
+
56
+ def self.fetch!(env, service, key, query_ssm=false)
57
+ if query_ssm
58
+ STDERR.puts "Using ssm to lookup secret"
59
+ ssm_val = self.fetch_single_secret_from_env_for_service!(env,service, key)
60
+ unless ssm_val
61
+ STDERR.puts "Could not find #{key} for #{service} in #{env} ssm"
62
+ exit 1
63
+ end
64
+ return ssm_val
65
+ end
66
+
67
+
68
+ unless PayCLI::Secrets::SECRETS_PROVIDERS.key? env
69
+ STDERR.puts "Could not find #{env} in secrets mapping #{SECRETS_FILE_PATH}"
70
+ exit 1
71
+ end
72
+
73
+ unless PayCLI::Secrets::SECRETS_PROVIDERS[env].key? service
74
+ STDERR.puts "Could not find #{service} in secrets mapping #{SECRETS_FILE_PATH} for #{env}"
75
+ exit 1
76
+ end
77
+
78
+ unless PayCLI::Secrets::SECRETS_PROVIDERS[env][service].key? key
79
+ STDERR.puts "Could not find #{key} in #{env} secrets mapping for #{service}"
80
+ exit 1
81
+ end
82
+
83
+ provider = PayCLI::Secrets::SECRETS_PROVIDERS[env][service][key][:provider]
84
+ STDERR.puts "Found provider #{provider} for key #{key} in #{env} for #{service}"
85
+
86
+ case provider
87
+ when 'local'
88
+ fetch_local! env, service, key
89
+ when 'generate'
90
+ generate! PayCLI::Secrets::SECRETS_PROVIDERS[env][service][key][:detail]
91
+ when 'value'
92
+ fetch_value! env, service, key
93
+ when 'pass'
94
+ fetch_pass! '', PayCLI::Secrets::SECRETS_PROVIDERS[env][service][key][:detail]
95
+ when /^pay-.*pass$/
96
+ pass_path = PayCLI::Secrets::SECRETS_PROVIDERS[env][service][key][:detail]
97
+ fetch_pay_pass! env, service, key, provider, pass_path
98
+ else
99
+ STDERR.puts "Provider #{provider} not supported"
100
+ exit 1
101
+ end
102
+ end
103
+
104
+ def self.generate!(detail)
105
+ puts "Generating #{detail}"
106
+ detail_array = detail.split(":")
107
+
108
+ abort "Incorrect format of generate detail #{detail}" unless detail_array.length == 2
109
+ method, length = detail_array
110
+ case method
111
+ when 'random'
112
+ SecureRandom.urlsafe_base64(length.to_i)
113
+ end
114
+ end
115
+
116
+ def self.fetch_pass!(path, pass_path)
117
+ env = {}
118
+ env['PASSWORD_STORE_DIR'] = path if path
119
+
120
+ stdin, stdout, wait_thr = Open3.popen2(env, "pass #{pass_path}")
121
+ password = stdout.readline.chomp
122
+ stdin.close
123
+ stdout.close
124
+ pass_status = wait_thr.value
125
+
126
+ abort "Pass failed, error above" unless pass_status.success?
127
+
128
+ password
129
+ end
130
+
131
+ def self.fetch_pay_pass!(env, service, key, provider, pass_path)
132
+ path = File.expand_path(File.join(
133
+ PayCLI::Config::PROJECT_PATH, '..', provider
134
+ ))
135
+
136
+ abort "Path #{path} doesn't exist" unless File.exist? path
137
+
138
+ STDERR.puts "Pulling secret #{key} from #{path} for #{service}"
139
+ fetch_pass! path, pass_path
140
+ end
141
+
142
+ def self.fetch_local!(env, service, key)
143
+ environments = YAML.load_file(PayCLI::Config::LOCAL_SECRETS_PATH)
144
+
145
+ value = environments.fetch(env, nil)&.fetch(service, nil)&.fetch(key, nil)
146
+ if value.nil?
147
+ STDERR.puts "Could not find #{key} in #{env} in local provider for #{service}"
148
+ exit 1
149
+ end
150
+
151
+ value
152
+ end
153
+
154
+ def self.fetch_value!(env, service, key)
155
+ PayCLI::Secrets::SECRETS_PROVIDERS
156
+ .fetch(env)
157
+ .fetch(service)
158
+ .fetch(key)
159
+ .fetch(:detail)
160
+ end
161
+
162
+ def self.secrets_for_service(service)
163
+ secrets_definition_path = File.join(
164
+ PayCLI::Config::CONFIG_PATH, 'service_secrets.yml'
165
+ )
166
+ secrets_definitions = YAML.load_file(secrets_definition_path)
167
+
168
+ unless secrets_definitions.key? service
169
+ abort "Could not find secrets definition for #{service}"
170
+ end
171
+
172
+ secrets_definitions.fetch service
173
+ end
174
+
175
+ def self.fetch_single_secret_from_env_for_service!(env, service, name)
176
+ PayCLI::Environment.setup! env
177
+ ssm = Aws::SSM::Client.new
178
+
179
+ begin
180
+ secret_value = ssm.get_parameter({
181
+ name: secret_name(env, service, name),
182
+ with_decryption: true
183
+ }).parameter.value
184
+ rescue Aws::SSM::Errors::ParameterNotFound
185
+ secret_value = nil
186
+ end
187
+
188
+ secret_value
189
+ end
190
+
191
+ def self.secrets_in_envs_for_service(envs, service)
192
+ accounts = envs.map { |e| e.split('-').first }.uniq
193
+ all_parameters = []
194
+
195
+ accounts.each do |acc|
196
+ PayCLI::Environment.setup! acc
197
+ ssm = Aws::SSM::Client.new
198
+
199
+ next_token = nil
200
+ loop do
201
+ STDERR.puts "Making request to AWS SSM for #{acc}"
202
+
203
+ opts = {}
204
+ opts[:next_token] = next_token unless next_token.nil?
205
+
206
+ response = ssm.describe_parameters(opts)
207
+
208
+ next_token = response.next_token
209
+ all_parameters += response.parameters
210
+
211
+ break if next_token.nil?
212
+ end
213
+ end
214
+
215
+ secrets_per_env = envs.map do |env|
216
+ secrets = all_parameters
217
+ .select { |p| p.name =~ /^#{env}_#{service}\./ }
218
+ .map { |p| p.name }
219
+ .map { |p| p.sub(/^#{env}_#{service}\./, '') }
220
+ .map { |p| p.upcase }
221
+ [env, secrets]
222
+ end.to_h
223
+
224
+ secrets_per_env
225
+ end
226
+
227
+ def self.secret_name(env, service, name)
228
+ return _concourse_secret_name(service, name) if %w[cd-pay-dev cd-pay-deploy cd-main].include? service
229
+
230
+ "#{env}_#{service}.#{name}".downcase
231
+ end
232
+
233
+ def self._concourse_secret_name(service, name)
234
+ # Service name is `cd-pay-dev` but the ssm name is only `pay-dev` so remove the cd-
235
+ service = service[3..]
236
+
237
+ "/pay-cd/concourse/pipelines/#{service.downcase}/#{name}"
238
+ end
239
+
240
+ def self.write_secret_for_service_in_env!(env, service, name, value)
241
+ PayCLI::Environment.setup! env
242
+ ssm = Aws::SSM::Client.new
243
+
244
+ STDERR.puts "Updating value of #{name} in #{env} with #{value}"
245
+ ssm.put_parameter({
246
+ name: secret_name(env, service, name),
247
+ type: 'SecureString',
248
+ value: value,
249
+ overwrite: true,
250
+ })
251
+ end
252
+
253
+ def self.fetch_secret_for_service_from_env(env, service, name)
254
+ PayCLI::Environment.setup! env
255
+ ssm = Aws::SSM::Client.new
256
+
257
+ STDERR.puts "Fetching secret #{name} from #{env} / #{service}"
258
+
259
+ ssm.get_parameter({
260
+ name: secret_name(env, service, name),
261
+ with_decryption: true
262
+ }).parameter.value
263
+ end
264
+
265
+ def self.diff_table(old_value, new_value)
266
+ TTY::Table.new([
267
+ ['Old', '=>', 'New'],
268
+ [old_value, '', new_value]
269
+ ]).render(
270
+ :unicode,
271
+ alignment: :center,
272
+ padding: 1,
273
+ multiline: true,
274
+ )
275
+ end
276
+ end
@@ -0,0 +1,10 @@
1
+ class PayCLI::StopYubicoAuthenticator
2
+ def self.stop_yubico_authenticator!
3
+ if system("killall -0 yubioath-desktop")
4
+ puts "❓ Yubico Authenticator appears to be running. It can interfere with establishing SSH connections."
5
+ if (STDERR.print " To stop Yubico Authenticator type 'yes' exactly > "; STDIN.gets.chomp == 'yes')
6
+ system("killall yubioath-desktop")
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,70 @@
1
+ require "tty-prompt"
2
+ require 'open3'
3
+
4
+ class PayCLI::YkmanOathCredentialConfig
5
+ attr_reader :config_file_path, :logger
6
+
7
+ def initialize(config_file_path: nil, logger: PayCLI::Logger)
8
+ @config_file_path = config_file_path || default_config_file_path
9
+ @logger = logger
10
+ end
11
+
12
+ def lookup(account)
13
+ load!
14
+ if @config[:ykman_defaults].has_key?(account)
15
+ yk_name = @config[:ykman_defaults][account]
16
+ logger.info "Oath code name '#{yk_name}' is selected for '#{account}' in '#{config_file_path}'"
17
+ yk_name
18
+ else
19
+ prompt(account)
20
+ end
21
+ end
22
+
23
+ def save(account, yk_name)
24
+ load!
25
+ @config[:ykman_defaults][account] = yk_name
26
+ logger.info "Saved your preferred OATH code name for '#{account}' to '#{config_file_path}'"
27
+ save!
28
+ end
29
+
30
+ def prompt(account)
31
+ output, err, status = Open3.capture3(%{ykman oath accounts list})
32
+
33
+ yk_names = output.lines.map {|line| line.gsub(/ +([0-9]+|\[Touch Credential\])$/,"").chomp }
34
+
35
+ prompt = TTY::Prompt.new
36
+ yk_name = prompt.select("Which code from your yubikey would you like to use for the '#{account}' account?", yk_names)
37
+
38
+ if prompt.yes?("Remember this choice?")
39
+ save(account, yk_name)
40
+ end
41
+
42
+ yk_name
43
+ end
44
+
45
+ private
46
+ def default_config_file_path
47
+ Pathname.new(Dir.home) + ".govuk_pay_cli_config.yml"
48
+ end
49
+
50
+ def load!
51
+ @config ||= YAML.load(File.read(config_file_path))
52
+ rescue Errno::ENOENT
53
+ @config ||= {ykman_defaults: {}}
54
+ end
55
+
56
+ def save!
57
+ File.write(config_file_path, yaml_preamble + YAML.dump(@config))
58
+ end
59
+
60
+ def yaml_preamble
61
+ <<-YAML_PREAMBLE
62
+ # This file is automatically generated by the GOV.UK Pay CLI
63
+ # You can edit settings here if you want
64
+ #
65
+ # For more info see https://github.com/alphagov/pay-infra/tree/master/cli
66
+ #
67
+ YAML_PREAMBLE
68
+ end
69
+
70
+ end
@@ -0,0 +1,5 @@
1
+ require 'zeitwerk'
2
+ loader = Zeitwerk::Loader.new
3
+ loader.push_dir(File.expand_path(__dir__))
4
+ loader.inflector.inflect "pay_cli" => "PayCLI"
5
+ loader.setup # ready!
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "cli",
3
+ "lockfileVersion": 2,
4
+ "requires": true,
5
+ "packages": {}
6
+ }
@@ -0,0 +1,149 @@
1
+ #!/bin/bash
2
+
3
+ # Temporary script for tunneling to RDS during development of the bastion solution
4
+
5
+ set -euo pipefail
6
+
7
+ function usage() {
8
+ echo "Usage: $0 <environment> <app>"
9
+ echo
10
+ echo "Examples:"
11
+ echo " $0 test-12 adminusers"
12
+ echo " $0 test-perf-1 ledger"
13
+ exit 1
14
+ }
15
+
16
+ function is_help_arg() {
17
+ if [ "$1" == "-h" ] || [ "$1" == "--help" ]; then
18
+ return 0
19
+ fi
20
+
21
+ return 1
22
+ }
23
+
24
+ if [ "$#" -ne 2 ]; then
25
+ usage
26
+ fi
27
+
28
+ ENVIRONMENT="$1"
29
+ APP="$2"
30
+ if [[ -z $ENVIRONMENT ]] || [[ -z $APP ]] || is_help_arg "$ENVIRONMENT" || is_help_arg "$APP"; then
31
+ usage
32
+ fi
33
+
34
+ for x in aws yq jq ssh-keygen aws-vault ssh
35
+ do
36
+ if ! command -v $x &> /dev/null
37
+ then
38
+ echo "$x is not installed, exiting."
39
+ exit 1
40
+ fi
41
+ done
42
+
43
+ ACCOUNT="${ENVIRONMENT%%-*}"
44
+ KEY_LOCATION="${TMPDIR}rds_tunnel_temp_key"
45
+
46
+ function cleanup() {
47
+ echo "cleaning up..."
48
+ if rm "${KEY_LOCATION}"*; then
49
+ echo "Removed temp key pair";
50
+ else
51
+ echo "Failed to remove key pair from ${KEY_LOCATION}"
52
+ fi
53
+ }
54
+
55
+ trap cleanup EXIT
56
+
57
+ echo "Finding bastion host"
58
+ read -r bastion_instance_id availability_zone <<< "$(aws-vault exec "$ACCOUNT" -- \
59
+ aws autoscaling describe-auto-scaling-groups | \
60
+ jq --arg env "$ENVIRONMENT" '.AutoScalingGroups[] | select(.AutoScalingGroupName == $env + "-bastion").Instances[0] | "\(.InstanceId) \(.AvailabilityZone)"' -r)"
61
+
62
+ if [[ -z $bastion_instance_id ]] || [[ -z $availability_zone ]]; then
63
+ echo "Failed to find bastion instance"
64
+ exit 1
65
+ fi
66
+
67
+ echo "Found bastion $bastion_instance_id"
68
+
69
+ echo "Getting RDS endpoint"
70
+ rds_endpoint="$(aws-vault exec "$ACCOUNT" -- \
71
+ aws rds describe-db-instances |\
72
+ jq --arg rds_instance "${ENVIRONMENT}-${APP}" '.DBInstances[] | select(.DBInstanceIdentifier | startswith($rds_instance)).Endpoint.Address' -r)"
73
+
74
+ if [[ -z $rds_endpoint ]]; then
75
+ echo "Failed to find RDS endpoint"
76
+ exit 1
77
+ fi
78
+ echo "RDS endpoint is: ${rds_endpoint}"
79
+
80
+ echo "Getting RDS instance engine version"
81
+ engine_version=$(aws-vault exec "$ACCOUNT" -- \
82
+ aws rds describe-db-instances | \
83
+ jq -r --arg rds_instance "${ENVIRONMENT}-${APP}" '.DBInstances[] | select(.DBInstanceIdentifier | startswith($rds_instance)).EngineVersion')
84
+ echo "RDS engine_version is ${engine_version}"
85
+
86
+ echo "Generating ssh key pair, saving to ${KEY_LOCATION}"
87
+ if ! ssh-keygen -q -t rsa -b 4096 -f "$KEY_LOCATION" -N ''; then
88
+ echo "Failed to generate ssh key pair"
89
+ exit 1
90
+ fi
91
+
92
+ echo "Uploading public key to bastion"
93
+ if ! aws-vault exec "$ACCOUNT" -- \
94
+ aws ec2-instance-connect send-ssh-public-key \
95
+ --instance-id "$bastion_instance_id" \
96
+ --availability-zone "$availability_zone" \
97
+ --instance-os-user ec2-user \
98
+ --ssh-public-key "file://${KEY_LOCATION}.pub"; then
99
+
100
+ echo "Failed to upload public key to bastion"
101
+ exit 1
102
+ fi
103
+
104
+ yellow="\033[0;33m"
105
+ reset="\033[0m"
106
+ ul="\033[4m"
107
+ ulstop="\033[24m"
108
+
109
+ echo -e "${yellow} ${reset}"
110
+ echo -e "${yellow} ⚠️ WARNING: When using SSM, any and all activity you perform may be getting logged for security auditing purposes (think PCI).${reset}"
111
+ echo -e "${yellow} Avoid sending or accessing ${ul}anything${ulstop} that could cause a security breach, such as:${reset}"
112
+ echo -e "${yellow} ${reset}"
113
+ echo -e "${yellow} • Secret API Keys or Tokens${reset}"
114
+ echo -e "${yellow} • Credentials or Passwords${reset}"
115
+ echo -e "${yellow} • Cardholder Data or Personally-Identifiable Information (PII)${reset}"
116
+ echo -e "${yellow} • Anything else that may be protected by GDPR or PCI-DSS${reset}"
117
+ echo -e "${yellow} • Anything classified as GSC 'Secret' or above${reset}"
118
+ echo -e "${yellow} ${reset}"
119
+ echo -e "${yellow} If you have a problem with this or aren\'t sure, use Ctrl-C ${ul}right now${ulstop} and discontinue your SSM session.${reset}"
120
+ echo -e "${yellow} ${reset}"
121
+
122
+ echo "Opening tunnel to rds"
123
+ if ! aws-vault exec "$ACCOUNT" -- \
124
+ ssh -i "$KEY_LOCATION" -N -f -M -S temp-ssh.sock \
125
+ -L 65432:"$rds_endpoint":5432 ec2-user@"$bastion_instance_id" \
126
+ -o "UserKnownHostsFile=/dev/null" \
127
+ -o "StrictHostKeyChecking=no" \
128
+ -o IdentitiesOnly=yes \
129
+ -o ProxyCommand="aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters portNumber=%p"; then
130
+ echo "Failed to open tunnel to RDS"
131
+ exit 1
132
+ fi
133
+
134
+ SOURCE_DIRECTORY=$(dirname "$BASH_SOURCE")
135
+ DB_USER=$(yq eval ".value.$ENVIRONMENT.$APP.DB_USER" < "${SOURCE_DIRECTORY}/../config/secrets.yml")
136
+
137
+ echo -e "Connected tunnel to $APP RDS database in $ENVIRONMENT on port 65432\n"
138
+ echo "Copy DB credentials to clipboard (in another window) using pay-low-pass:"
139
+ echo -e " pay-low-pass aws/rds/application_users/$ACCOUNT/$DB_USER | pbcopy\n"
140
+ echo "Alternatively, fetch credentials from pay secrets:"
141
+ echo -e " pay secrets fetch $ENVIRONMENT $APP DB_PASSWORD | pbcopy\n"
142
+ echo "Open psql with:"
143
+ echo -e " psql -h localhost -p 65432 -U $DB_USER -d $APP\n"
144
+ echo "Alternatively connect using docker instead of needing psql installed locally and set the password automatically using pay-low-pass:"
145
+ echo -e " docker run --rm -ti postgres:${engine_version}-alpine psql --host docker.for.mac.localhost --port 65432 --user $DB_USER --dbname $APP\n"
146
+ echo "Or even more conveniently connect using a docker container and set the password automatically using pay-low-pass:"
147
+ echo -e " docker run -e \"PGPASSWORD=\$(pay-low-pass aws/rds/application_users/${ACCOUNT}/${DB_USER})\" --rm -ti postgres:${engine_version}-alpine psql --host docker.for.mac.localhost --port 65432 --user $DB_USER --dbname $APP\n"
148
+ read -rsn1 -p "Press any key to close session."; echo
149
+ ssh -O exit -S temp-ssh.sock '*'
@@ -0,0 +1,2 @@
1
+ Metrics/BlockLength:
2
+ IgnoredMethods: ['describe', 'context']
@@ -0,0 +1,34 @@
1
+ # Preceeding comment
2
+
3
+
4
+ FROM node:18.18.0-alpine3.18@sha256:619ce27eb37c7c0476bd518085bf1ba892e2148fc1ab5dbaff2f20c56e50444d as builder
5
+
6
+ WORKDIR /app
7
+ COPY package.json .
8
+ COPY package-lock.json .
9
+ RUN npm ci --quiet
10
+
11
+ COPY . .
12
+ RUN npm run compile
13
+
14
+ FROM node:18.18.0-alpine3.18@sha256:619ce27eb37c7c0476bd518085bf1ba892e2148fc1ab5dbaff2f20c56e50444d as final
15
+
16
+ RUN ["apk", "--no-cache", "upgrade"]
17
+
18
+ RUN ["apk", "add", "--no-cache", "tini"]
19
+
20
+ WORKDIR /app
21
+ COPY . .
22
+ RUN rm -rf ./test
23
+ # Copy in compile assets and deps from build container
24
+ COPY --from=builder /app/node_modules ./node_modules
25
+ COPY --from=builder /app/govuk_modules ./govuk_modules
26
+ COPY --from=builder /app/public ./public
27
+ RUN npm prune --omit=dev
28
+
29
+ ENV PORT 9000
30
+ EXPOSE 9000
31
+
32
+ ENTRYPOINT ["tini", "--"]
33
+
34
+ CMD ["npm", "start"]
@@ -0,0 +1,33 @@
1
+ # Preceeding comment
2
+
3
+ FROM node:18.18.0-alpine3.18@sha256:619ce27eb37c7c0476bd518085bf1ba892e2148fc1ab5dbaff2f20c56e50444d as builder
4
+
5
+ WORKDIR /app
6
+ COPY package.json .
7
+ COPY package-lock.json .
8
+ RUN npm ci --quiet
9
+
10
+ COPY . .
11
+ RUN npm run compile
12
+
13
+ FROM node:18.18.0-alpine3.18@sha256:619ce27eb37c7c0476bd518085bf1ba892e2148fc1ab5dbaff2f20c56e50444d as final
14
+
15
+ RUN ["apk", "--no-cache", "upgrade"]
16
+
17
+ RUN ["apk", "add", "--no-cache", "tini"]
18
+
19
+ WORKDIR /app
20
+ COPY . .
21
+ RUN rm -rf ./test
22
+ # Copy in compile assets and deps from build container
23
+ COPY --from=builder /app/node_modules ./node_modules
24
+ COPY --from=builder /app/govuk_modules ./govuk_modules
25
+ COPY --from=builder /app/public ./public
26
+ RUN npm prune --omit=dev
27
+
28
+ ENV PORT 9000
29
+ EXPOSE 9000
30
+
31
+ ENTRYPOINT ["tini", "--"]
32
+
33
+ CMD ["npm", "start"]
@@ -0,0 +1,5 @@
1
+ FROM node:alpine-3.18
2
+
3
+ ENV foo=bar
4
+
5
+ CMD ["sh"]
@@ -0,0 +1,5 @@
1
+ FROM node:alpine-3.18@sha256:3482a20c97e401b56ac50ba8920cc7b5b2022bfc6aa7d4e4c231755770cf892f
2
+
3
+ ENV foo=bar
4
+
5
+ CMD ["sh"]
@@ -0,0 +1,5 @@
1
+ FROM node@sha256:b1fdeade9cae98c30bbd8087f26f8da404e6fc48bdd53772017855c1a1d32605
2
+
3
+ ENV foo=bar
4
+
5
+ CMD ["sh"]
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe PayCLI::Commands::Local::ImageExtractor do
4
+ describe '.parse_image_without_sha' do
5
+ it 'gives the correct image name and tag on a simple dockerfile' do
6
+ expect(
7
+ described_class.parse_image_without_sha(fixture_path('simple'))
8
+ ).to eq('node:alpine-3.18')
9
+ end
10
+
11
+ it 'gives the correct image name and tag on a simple dockerfile with no image tag' do
12
+ expect(
13
+ described_class.parse_image_without_sha(fixture_path('simple_no_tag'))
14
+ ).to eq('node')
15
+ end
16
+
17
+ it 'gives the correct image name and tag on a dockerfile with an image sha and tag' do
18
+ expect(
19
+ described_class.parse_image_without_sha(fixture_path('with_sha'))
20
+ ).to eq('node:alpine-3.18')
21
+ end
22
+
23
+ it 'gives the correct image name and tag on a dockerfile with an image sha but no tag' do
24
+ expect(
25
+ described_class.parse_image_without_sha(fixture_path('with_sha_no_tag'))
26
+ ).to eq('node')
27
+ end
28
+
29
+ it 'gives the correct image name and tag on a complex dockerfile with multiple FROM lines' do
30
+ expect(
31
+ described_class.parse_image_without_sha(fixture_path('complex'))
32
+ ).to eq('node:18.18.0-alpine3.18')
33
+ end
34
+
35
+ it 'gives the first image name and tag on a complex dockerfile with multiple differing FROM lines' do
36
+ expect(
37
+ described_class.parse_image_without_sha(fixture_path('complex_differing_froms'))
38
+ ).to eq('node:18.18.0-alpine3.18')
39
+ end
40
+
41
+ it 'raises a DockerfileNotFound error if the dockerfile does not exist' do
42
+ expect { described_class.parse_image_without_sha(fixture_path('DOES_NOT_EXIST')) }
43
+ .to raise_error(PayCLI::Commands::Local::ImageExtractor::DockerfileNotFound)
44
+ end
45
+
46
+ it 'raises an ImageNotFoundInDockerfile error if the file does not contain a FROM line' do
47
+ expect { described_class.parse_image_without_sha(fixture_path('no_from')) }
48
+ .to raise_error(PayCLI::Commands::Local::ImageExtractor::ImageNotFoundInDockerfile)
49
+ end
50
+ end
51
+
52
+ def fixture_path(dockerfile_name)
53
+ File.join(__dir__, '..', '..', '..', '..', 'fixtures', 'dockerfile_examples', "Dockerfile.#{dockerfile_name}")
54
+ end
55
+ end