@govuk-pay/cli 0.0.36 → 0.0.38

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@govuk-pay/cli",
3
- "version": "0.0.36",
3
+ "version": "0.0.38",
4
4
  "description": "GOV.UK Pay Command Line Interface",
5
5
  "bin": {
6
6
  "pay": "bin/cli.js",
@@ -7,12 +7,12 @@ exports.handler = exports.builder = exports.desc = exports.command = void 0;
7
7
  const app_client_js_1 = __importDefault(require("../app_client/app_client.js"));
8
8
  const standardContent_1 = require("../../../core/standardContent");
9
9
  exports.command = 'account';
10
- exports.desc = 'Create a local gateway account';
10
+ exports.desc = 'Create a local gateway account, and optionally a new service';
11
11
  const builder = (yargs) => {
12
12
  return yargs.usage(`$0 local account [--service-id <SERVICE_ID>]\n\n${exports.desc}`)
13
13
  .option('service-id', {
14
14
  type: 'string',
15
- description: 'ID of an existing service'
15
+ description: 'External ID of an existing service, if not provided a new service will be created'
16
16
  });
17
17
  };
18
18
  exports.builder = builder;
@@ -9,18 +9,18 @@ const app_client_js_1 = __importDefault(require("../app_client/app_client.js"));
9
9
  const pay_local_cluster_js_1 = require("../config/pay_local_cluster.js");
10
10
  const standardContent_1 = require("../../../core/standardContent");
11
11
  exports.command = 'browse <service>';
12
- exports.desc = 'Browse to local service';
12
+ exports.desc = 'Open a local service in your web browser';
13
13
  const builder = (yargs) => {
14
14
  return yargs
15
15
  .usage(`$0 local browse <service> [--proxy]\n\n${exports.desc}`)
16
16
  .positional('service', {
17
17
  type: 'string',
18
- description: 'service'
18
+ description: 'Name of the service to open in your browser'
19
19
  })
20
20
  .option('proxy', {
21
21
  type: 'boolean',
22
22
  default: false,
23
- description: 'Opens the service via the local proxy for the service'
23
+ description: 'Opens the service via the reverse proxy for the service'
24
24
  });
25
25
  };
26
26
  exports.builder = builder;
@@ -4,14 +4,16 @@ exports.handler = exports.builder = exports.desc = exports.command = void 0;
4
4
  const node_child_process_1 = require("node:child_process");
5
5
  const pay_local_cluster_js_1 = require("../config/pay_local_cluster.js");
6
6
  const standardContent_1 = require("../../../core/standardContent");
7
+ const servicesConfig = (0, pay_local_cluster_js_1.loadServicesConfig)();
7
8
  exports.command = 'db <app_name>';
8
- exports.desc = 'Connect to <app_name> database';
9
+ exports.desc = 'Open psql connected to <app_name> database';
9
10
  const builder = (yargs) => {
10
11
  return yargs
11
12
  .usage(`$0 local db <app_name> [--docker]\n\n${exports.desc}`)
12
13
  .positional('app_name', {
13
14
  type: 'string',
14
- description: 'service'
15
+ choices: Object.values(servicesConfig).filter(service => service.db).map(service => service.name).sort(),
16
+ description: 'The name of the app whose database you wish to connect to'
15
17
  })
16
18
  .option('docker', {
17
19
  type: 'boolean',
@@ -25,7 +27,6 @@ async function dbHandler(argv) {
25
27
  await (0, standardContent_1.showHeader)();
26
28
  const service = argv.app_name;
27
29
  const docker = argv.docker;
28
- const servicesConfig = (0, pay_local_cluster_js_1.loadServicesConfig)();
29
30
  if (!(service in servicesConfig)) {
30
31
  console.error(`The service specified (${service}) was not defined in the service_config.yaml file`);
31
32
  return;
@@ -15,13 +15,13 @@ exports.DOCKER_COMPOSE_RENDERED_TEMPALTES_PATH = node_path_1.default.join('local
15
15
  exports.DOCKER_COMPOSE_SERVICES_CONFIG_PATH = node_path_1.default.join('local', 'docker-compose', 'default-configs-DO-NOT-EDIT');
16
16
  exports.ENVIRONMENT_OVERRIDES_PATH = node_path_1.default.join('local', 'environment-overrides');
17
17
  exports.command = 'down';
18
- exports.desc = 'Bring down a cluster';
18
+ exports.desc = 'Shut down the local cluster';
19
19
  const builder = (yargs) => {
20
20
  return yargs
21
21
  .usage(`$0 local down [--cluster <cluster>]\n\n${exports.desc}`)
22
22
  .option('cluster', {
23
23
  type: 'string',
24
- description: 'Cluster to bring down',
24
+ description: 'Cluster to bring down, if not specified it will be the last cluster that was launched',
25
25
  choices: pay_local_cluster_js_1.CLUSTERS
26
26
  });
27
27
  };
@@ -12,7 +12,7 @@ exports.DOCKER_COMPOSE_RENDERED_TEMPALTES_PATH = node_path_1.default.join('local
12
12
  exports.DOCKER_COMPOSE_SERVICES_CONFIG_PATH = node_path_1.default.join('local', 'docker-compose', 'default-configs-DO-NOT-EDIT');
13
13
  exports.ENVIRONMENT_OVERRIDES_PATH = node_path_1.default.join('local', 'environment-overrides');
14
14
  exports.command = 'nuke';
15
- exports.desc = 'Kill and completely destroy all containers previously started by pay local';
15
+ exports.desc = 'Kill and completely destroy all containers that can be launched by pay local';
16
16
  exports.handler = nukeHandler;
17
17
  async function nukeHandler(argv) {
18
18
  await (0, standardContent_1.showHeader)();
@@ -5,11 +5,11 @@ const node_child_process_1 = require("node:child_process");
5
5
  const totp_generator_1 = require("totp-generator");
6
6
  const standardContent_1 = require("../../../core/standardContent");
7
7
  exports.command = 'otp <key>';
8
- exports.desc = 'Create otp code';
8
+ exports.desc = 'Generate an otp code';
9
9
  const builder = (yargs) => {
10
10
  return yargs.positional('key', {
11
11
  type: 'string',
12
- description: 'otp key'
12
+ description: 'TOTP secret key for which to generated a code'
13
13
  });
14
14
  };
15
15
  exports.builder = builder;
@@ -14,7 +14,7 @@ const builder = (yargs) => {
14
14
  .usage(`$0 local payment [--api-key <api-key>] [--email-collection-mode <email-collection-mode>]\n\n${exports.desc}`)
15
15
  .option('api-key', {
16
16
  type: 'string',
17
- description: 'API Key'
17
+ description: 'API Key to use to create the payment. If not set a new service and gateway account will also be created'
18
18
  }).option('email-collection-mode', {
19
19
  type: 'string',
20
20
  default: 'MANDATORY',
@@ -8,14 +8,14 @@ const node_child_process_1 = __importDefault(require("node:child_process"));
8
8
  const app_client_js_1 = __importDefault(require("../app_client/app_client.js"));
9
9
  const standardContent_1 = require("../../../core/standardContent");
10
10
  exports.command = 'paymentlink';
11
- exports.desc = 'Create a payment link';
11
+ exports.desc = 'Create a payment link in gateway account id 1';
12
12
  const builder = (yargs) => {
13
13
  return yargs
14
14
  .usage(`$0 local paymentlink --api-key <api-key>\n\n${exports.desc}`)
15
15
  .option('api-key', {
16
16
  demandOption: true,
17
17
  type: 'string',
18
- description: 'API Key'
18
+ description: 'API Key for the payment link to use to create payments'
19
19
  });
20
20
  };
21
21
  exports.builder = builder;
@@ -11,13 +11,13 @@ const docker_compose_controller_js_1 = __importDefault(require("../docker_compos
11
11
  const standardContent_1 = require("../../../core/standardContent");
12
12
  exports.DOCKER_COMPOSE_RENDERED_TEMPLATES_PATH = node_path_1.default.join('local', 'docker-compose', 'rendered-templates');
13
13
  exports.command = 'restart <app>';
14
- exports.desc = 'Restart a container and waits for it to be healthy';
14
+ exports.desc = 'Restart a container and wait for it to be healthy';
15
15
  const builder = (yargs) => {
16
16
  return yargs
17
17
  .usage(`$0 local restart <app>\n\n${exports.desc}`)
18
18
  .positional('app', {
19
19
  type: 'string',
20
- description: 'App to restart'
20
+ description: 'Container to restart'
21
21
  });
22
22
  };
23
23
  exports.builder = builder;
@@ -8,13 +8,13 @@ const node_child_process_1 = require("node:child_process");
8
8
  const app_client_js_1 = __importDefault(require("../app_client/app_client.js"));
9
9
  const standardContent_1 = require("../../../core/standardContent");
10
10
  exports.command = 'token';
11
- exports.desc = 'Create a token';
11
+ exports.desc = 'Create an API token';
12
12
  const builder = (yargs) => {
13
13
  return yargs
14
14
  .usage(`$0 local token [--gateway-account-id <gateway-account-id>]\n\n${exports.desc}`)
15
15
  .option('gateway-account-id', {
16
16
  type: 'string',
17
- description: 'ID of the gateway account'
17
+ description: 'ID of the gateway account, if not specified a service and gateway account will also be created'
18
18
  });
19
19
  };
20
20
  exports.builder = builder;
@@ -24,42 +24,42 @@ const builder = (yargs) => {
24
24
  type: 'string',
25
25
  choices: pay_local_cluster_js_1.CLUSTERS,
26
26
  default: 'all',
27
- description: 'cluster'
27
+ description: 'Cluster of services to launch'
28
28
  })
29
29
  .option('apps', {
30
30
  type: 'array',
31
31
  default: [],
32
- description: ''
32
+ description: 'Launch only these listed apps within the specified cluster'
33
33
  })
34
34
  .option('local', {
35
35
  type: 'array',
36
36
  default: [],
37
- description: ''
37
+ description: 'Use local versions of the docker images from the checkouts in your WORKSPACE'
38
38
  })
39
39
  .option('proxy', {
40
40
  type: 'boolean',
41
41
  default: false,
42
- description: ''
42
+ description: 'Launch reverse proxies with naxsi configured in front of the public-facing apps'
43
43
  })
44
44
  .option('with-egress-proxy', {
45
45
  type: 'boolean',
46
46
  default: false,
47
- description: ''
47
+ description: 'Launch the cluster with the egress proxy and configure connector and frontend to use it'
48
48
  })
49
49
  .option('rebuild', {
50
50
  type: 'boolean',
51
51
  default: true,
52
- description: ''
52
+ description: 'When running with --local, force the local containers to be rebuilt from your WORKSPACE'
53
53
  })
54
54
  .option('pull', {
55
55
  type: 'boolean',
56
56
  default: true,
57
- description: ''
57
+ description: 'Pull the latest versions of all containers prior to launching the cluster'
58
58
  })
59
59
  .option('mount-local-node-apps', {
60
60
  type: 'boolean',
61
61
  default: false,
62
- description: ''
62
+ description: 'Mount your local project directory (in WORKSPACE/pay-<app>) into the running node container'
63
63
  })
64
64
  .option('mount-pay-js-commons', {
65
65
  type: 'boolean',
@@ -13,11 +13,11 @@ exports.desc = 'Copy base URL of local service to clipboard';
13
13
  const builder = (yargs) => {
14
14
  return yargs.positional('service', {
15
15
  type: 'string',
16
- description: 'service'
16
+ description: 'The service you wish to generate the URL of'
17
17
  }).option('proxy', {
18
18
  type: 'boolean',
19
19
  default: false,
20
- description: 'Something about proxy'
20
+ description: 'Generate the url for the reverse proxy of the <service> specified'
21
21
  });
22
22
  };
23
23
  exports.builder = builder;
@@ -9,14 +9,14 @@ const totp_generator_1 = require("totp-generator");
9
9
  const node_child_process_1 = require("node:child_process");
10
10
  const standardContent_1 = require("../../../core/standardContent");
11
11
  exports.command = 'user';
12
- exports.desc = 'Create a local user';
12
+ exports.desc = 'Create a selfservice user with a new service and gateway account';
13
13
  const builder = (yargs) => {
14
14
  return yargs
15
15
  .usage(`$0 local user [--create-payments]\n\n${exports.desc}`)
16
16
  .option('create-payments', {
17
17
  type: 'boolean',
18
- default: false,
19
- description: 'Create payments'
18
+ default: true,
19
+ description: 'Create 10 payments in the gateway account'
20
20
  });
21
21
  };
22
22
  exports.builder = builder;
@@ -48,7 +48,7 @@ async function userHandler(argv) {
48
48
  console.error('Failed to create an API token');
49
49
  return;
50
50
  }
51
- if (createPayments !== undefined && createPayments) {
51
+ if (createPayments) {
52
52
  console.warn('💸 Creating 10 payments in card sandbox gateway account (each . is a success, each ! is a failure)');
53
53
  let createPaymentResult;
54
54
  for (let i = 0; i < 10; i++) {
@@ -60,6 +60,7 @@ async function userHandler(argv) {
60
60
  process.stderr.write('.');
61
61
  }
62
62
  }
63
+ console.warn('');
63
64
  }
64
65
  console.warn('🤓 Creating admin user for service');
65
66
  const user = await app_client_js_1.default.createUser([account.gateway_account_id], 'admin');
@@ -82,14 +83,18 @@ async function userHandler(argv) {
82
83
  console.log(`🎫 Card API token: ${apiToken}`);
83
84
  console.log();
84
85
  let selfserviceUrl;
86
+ copyToClipboard(user.email);
87
+ console.log(`📋 “${user.email}” copied to clipboard`);
85
88
  if (await app_client_js_1.default.isProxyHealthy('selfservice')) {
86
89
  selfserviceUrl = app_client_js_1.default.externalUrlFor('selfservice', '/', true);
87
90
  }
88
- else {
91
+ else if (await app_client_js_1.default.isHealthy('selfservice')) {
89
92
  selfserviceUrl = app_client_js_1.default.externalUrlFor('selfservice', '/', false);
90
93
  }
91
- copyToClipboard(user.email);
92
- console.log(`📋 “${user.email}” copied to clipboard`);
94
+ else {
95
+ console.error('The selfservice service is unhealthy, so it is not being opened in a web browser');
96
+ return;
97
+ }
93
98
  console.log(`🌎 Opening selfservice (${selfserviceUrl}) browser`);
94
99
  console.log();
95
100
  (0, node_child_process_1.spawn)('open', [selfserviceUrl]);
@@ -16,13 +16,15 @@ const constants_js_1 = require("../core/constants.js");
16
16
  let ec2;
17
17
  let ecs;
18
18
  let ssm;
19
+ let rds;
19
20
  const FORMAT = {
20
21
  red: '\x1b[31m',
21
22
  green: '\x1b[32m',
22
23
  yellow: '\x1b[33m',
23
24
  reset: '\x1b[0m',
24
25
  ul: '\x1b[4m',
25
- ulstop: '\x1b[24m'
26
+ ulstop: '\x1b[24m',
27
+ bel: '\u0007'
26
28
  };
27
29
  exports.command = 'tunnel <environment> <application>';
28
30
  exports.desc = 'Open a tunnel to an app database in a given environment';
@@ -66,18 +68,37 @@ async function tunnelHandler(argv) {
66
68
  const environment = argv.environment;
67
69
  const application = argv.application;
68
70
  console.log(`Opening a database tunnel to ${environment} ${application}`);
69
- ec2 = new client_ec2_1.EC2Client();
70
- ecs = new client_ecs_1.ECSClient();
71
- ssm = new client_ssm_1.SSMClient();
71
+ let maximumDuration = 90;
72
+ if (process.env.MAXIMUM_DURATION !== undefined) {
73
+ maximumDuration = parseInt(process.env.MAXIMUM_DURATION);
74
+ if (isNaN(maximumDuration)) {
75
+ maximumDuration = 90;
76
+ console.log(`${FORMAT.yellow}MAXIMUM_DURATION should be a whole number of minutes, e.g. MAXIMUM_DURATION=60. Defaulting to ${maximumDuration}.${FORMAT.reset}`);
77
+ }
78
+ }
79
+ const warnTime = maximumDuration * 0.75;
80
+ ec2 = new client_ec2_1.EC2Client({ region: 'eu-west-1' });
81
+ ecs = new client_ecs_1.ECSClient({ region: 'eu-west-1' });
82
+ ssm = new client_ssm_1.SSMClient({ region: 'eu-west-1' });
83
+ rds = new client_rds_1.RDSClient({ region: 'eu-west-1' });
72
84
  let bastionTask = null;
73
85
  try {
74
86
  printWarningToUser();
75
87
  const dbAccessType = await readInputForReadOrWriteDBAccess();
76
88
  const database = await getDatabaseDetails(environment, application);
77
- bastionTask = await startBastion(environment);
89
+ bastionTask = await startBastion(environment, maximumDuration);
90
+ console.log(`${FORMAT.bel}${FORMAT.yellow}The bastion service will terminate in ${maximumDuration} minutes.${FORMAT.reset}`);
91
+ const warnTimeout = setTimeout(() => {
92
+ console.log(`${FORMAT.bel}${FORMAT.yellow}The bastion service will terminate in ${maximumDuration - warnTime} minutes.${FORMAT.reset}`);
93
+ }, warnTime * 60 * 1000);
94
+ const exitTimeout = setTimeout(() => {
95
+ console.log(`${FORMAT.bel}${FORMAT.yellow}The bastion service will now terminate, your tunnel may stop unexpectedly.${FORMAT.reset}`);
96
+ }, maximumDuration * 60 * 1000);
78
97
  openTunnel(bastionTask, database, environment);
79
98
  await printHowToTunnelText(application, environment, database.EngineVersion, dbAccessType);
80
99
  await waitForExit();
100
+ clearTimeout(warnTimeout);
101
+ clearTimeout(exitTimeout);
81
102
  await shutdown(environment, bastionTask);
82
103
  }
83
104
  catch (error) {
@@ -117,10 +138,10 @@ async function waitForExit() {
117
138
  });
118
139
  });
119
140
  }
120
- async function startBastion(environment) {
141
+ async function startBastion(environment, maximumDuration) {
121
142
  const subnetIds = await getBastionSubNets(environment);
122
143
  const securityGroupIds = await getBastionSecurityGroups(environment);
123
- let bastionTask = await launchBastionTask(environment, subnetIds, securityGroupIds);
144
+ let bastionTask = await launchBastionTask(environment, subnetIds, securityGroupIds, maximumDuration);
124
145
  bastionTask = await waitForBastionToBeReady(bastionTask, environment);
125
146
  return bastionTask;
126
147
  }
@@ -156,12 +177,29 @@ async function getBastionSecurityGroups(environment) {
156
177
  .filter(s => s.GroupId !== undefined)
157
178
  .map((securityGroup) => securityGroup.GroupId);
158
179
  }
159
- async function launchBastionTask(environment, subnetIds, securityGroupIds) {
180
+ async function launchBastionTask(environment, subnetIds, securityGroupIds, maximumDuration) {
181
+ let taskDefinition = `${environment}-bastion`;
182
+ if (process.env.BASTION_TASK_DEF_NAME != null && process.env.BASTION_TASK_DEF_NAME.length > 0) {
183
+ taskDefinition = process.env.BASTION_TASK_DEF_NAME;
184
+ }
160
185
  const runTaskCommand = new client_ecs_1.RunTaskCommand({
161
186
  cluster: `${environment}-fargate`,
162
- taskDefinition: `${environment}-bastion`,
187
+ taskDefinition,
163
188
  launchType: 'FARGATE',
164
189
  enableExecuteCommand: true,
190
+ overrides: {
191
+ containerOverrides: [
192
+ {
193
+ name: `${environment}-bastion`,
194
+ environment: [
195
+ {
196
+ name: 'MAXIMUM_DURATION',
197
+ value: `${maximumDuration}m`
198
+ }
199
+ ]
200
+ }
201
+ ]
202
+ },
165
203
  networkConfiguration: {
166
204
  awsvpcConfiguration: {
167
205
  subnets: subnetIds,
@@ -259,7 +297,6 @@ async function stopBastion(task, environment) {
259
297
  }
260
298
  async function getDatabaseDetails(environment, application) {
261
299
  const describeDbCommand = new client_rds_1.DescribeDBInstancesCommand({});
262
- const rds = new client_rds_1.RDSClient();
263
300
  const describeDbCommandResponse = await rds.send(describeDbCommand);
264
301
  if (describeDbCommandResponse.DBInstances == null || describeDbCommandResponse.DBInstances.length < 1) {
265
302
  throw new Error(`Failed to find the database for ${application} in ${environment}`);
@@ -384,10 +421,9 @@ async function getDbUser(environment, application, dbAccessType) {
384
421
  else {
385
422
  paramName = `${environment}_${application}.db_support_user_readonly`;
386
423
  }
387
- const ssmClient = new client_ssm_1.SSMClient();
388
424
  const input = { Names: [paramName], WithDecryption: true };
389
425
  const command = new client_ssm_1.GetParametersCommand(input);
390
- const response = await ssmClient.send(command);
426
+ const response = await ssm.send(command);
391
427
  if (response?.Parameters?.length !== undefined && response?.Parameters?.length > 0 &&
392
428
  response?.Parameters[0]?.Value !== undefined) {
393
429
  return response.Parameters[0].Value;
@@ -430,5 +466,5 @@ function printError(error) {
430
466
  console.error(`${FORMAT.red}${error}${FORMAT.reset}`);
431
467
  }
432
468
  function printGreen(message) {
433
- console.error(`${FORMAT.green}${message}${FORMAT.reset}`);
469
+ console.log(`${FORMAT.green}${message}${FORMAT.reset}`);
434
470
  }