@govuk-pay/cli 0.0.51 → 0.0.53
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 +2 -1
- package/readme.md +11 -0
- package/src/commands/secrets/config/config.types.js +45 -0
- package/src/commands/secrets/config/secrets/pay_low_pass/deploy-7.js +9 -0
- package/src/commands/secrets/config/secrets/pay_low_pass/deploy-tooling.js +18 -0
- package/src/commands/secrets/config/secrets/pay_low_pass/deploy.js +64 -0
- package/src/commands/secrets/config/secrets/pay_low_pass/dev.js +13 -0
- package/src/commands/secrets/config/secrets/pay_low_pass/production-2.js +104 -0
- package/src/commands/secrets/config/secrets/pay_low_pass/production.js +8 -0
- package/src/commands/secrets/config/secrets/pay_low_pass/staging-2.js +98 -0
- package/src/commands/secrets/config/secrets/pay_low_pass/staging.js +8 -0
- package/src/commands/secrets/config/secrets/pay_low_pass/test-12.js +101 -0
- package/src/commands/secrets/config/secrets/pay_low_pass/test-perf-1.js +98 -0
- package/src/commands/secrets/config/secrets/pay_low_pass/test.js +13 -0
- package/src/commands/secrets/config/secrets/pay_low_pass.js +27 -0
- package/src/commands/secrets/config/secrets/ssm.js +4 -0
- package/src/commands/secrets/config/secrets/value/deploy-tooling.js +10 -0
- package/src/commands/secrets/config/secrets/value/deploy.js +20 -0
- package/src/commands/secrets/config/secrets/value/production-2.js +45 -0
- package/src/commands/secrets/config/secrets/value/staging-2.js +47 -0
- package/src/commands/secrets/config/secrets/value/test-12.js +47 -0
- package/src/commands/secrets/config/secrets/value/test-perf-1.js +49 -0
- package/src/commands/secrets/config/secrets/value.js +17 -0
- package/src/commands/secrets/config/secrets.js +83 -0
- package/src/commands/secrets/config/service_secrets.js +238 -0
- package/src/commands/secrets/providers/factory.js +36 -0
- package/src/commands/secrets/providers/pass_repo.js +65 -0
- package/src/commands/secrets/providers/providers.types.js +21 -0
- package/src/commands/secrets/providers/ssm.js +155 -0
- package/src/commands/secrets/providers/value.js +10 -0
- package/src/commands/secrets/subcommands/audit.js +41 -9
- package/src/commands/secrets/subcommands/fetch.js +36 -15
- package/src/commands/secrets/subcommands/provision.js +99 -7
- package/src/commands/secrets.js +1 -1
- package/src/core/commandRouter.js +1 -0
- package/src/core/standardContent.js +5 -1
- package/src/util/configs.js +7 -1
- package/src/commands/secrets/subcommands/copy.js +0 -35
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.PassRepoProvider = void 0;
|
|
7
|
+
const node_child_process_1 = __importDefault(require("node:child_process"));
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const providers_types_1 = require("./providers.types");
|
|
11
|
+
const configs_1 = require("../../../util/configs");
|
|
12
|
+
class PassRepoProvider extends providers_types_1.AbstractSecretProvider {
|
|
13
|
+
passRepoDirectory;
|
|
14
|
+
constructor(environment, secretSource) {
|
|
15
|
+
super(environment, secretSource);
|
|
16
|
+
this.passRepoDirectory = this.getPassDirectory();
|
|
17
|
+
}
|
|
18
|
+
async get(secretConfig) {
|
|
19
|
+
const passCommandResult = node_child_process_1.default.spawnSync('pass', [secretConfig.secretSourceValue], {
|
|
20
|
+
env: {
|
|
21
|
+
...process.env,
|
|
22
|
+
PASSWORD_STORE_DIR: this.passRepoDirectory
|
|
23
|
+
},
|
|
24
|
+
// This sets stdin, to be inherited by the spawned process, this is neccessary so that
|
|
25
|
+
// the pass command can prompt the user for a PIN.
|
|
26
|
+
// However we need to capture the stdout and stderr (which pass writes the secret to) so we set
|
|
27
|
+
// that to be pipe which will store it in the results .output Buffer[], .stdout Buffer, and .stderr Buffer
|
|
28
|
+
stdio: [
|
|
29
|
+
'inherit', // stdin
|
|
30
|
+
'pipe', // stdout
|
|
31
|
+
'pipe' // stderr
|
|
32
|
+
]
|
|
33
|
+
});
|
|
34
|
+
if (passCommandResult.status === null) {
|
|
35
|
+
if (passCommandResult.signal === null) {
|
|
36
|
+
console.error(`The pass command to load the secret ${secretConfig.secretSourceValue} failed due to an unknown reason. It's output follows:`);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
console.error(`The pass command to load the secret ${secretConfig.secretSourceValue} was terminated by signal ${passCommandResult.signal}. It's output follows`);
|
|
40
|
+
}
|
|
41
|
+
console.error(passCommandResult.output.map((buffer) => { return buffer === null ? '' : buffer.toString('utf-8'); }).join('\n'));
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
if (passCommandResult.status !== 0) {
|
|
45
|
+
const stdErrString = passCommandResult.output.map((buffer) => { return buffer === null ? '' : buffer.toString('utf-8'); }).join('\n');
|
|
46
|
+
if (stdErrString.includes('is not in the password store')) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
console.error(`The pass command to load the secret ${secretConfig.secretSourceValue} failed with exit code ${passCommandResult.status}. It's output follows:`);
|
|
50
|
+
console.error(stdErrString);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
return passCommandResult.stdout.toString('utf-8').trim();
|
|
54
|
+
}
|
|
55
|
+
getPassDirectory() {
|
|
56
|
+
const worksspaceDir = (0, configs_1.workspaceEnvVar)();
|
|
57
|
+
const passRepoDirectory = node_path_1.default.resolve(node_path_1.default.join(worksspaceDir, this.secretSource));
|
|
58
|
+
const pathStat = node_fs_1.default.lstatSync(passRepoDirectory);
|
|
59
|
+
if (!pathStat.isDirectory()) {
|
|
60
|
+
throw new Error(`The pass repo directory specified ${passRepoDirectory} is not a directory`);
|
|
61
|
+
}
|
|
62
|
+
return passRepoDirectory;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
exports.PassRepoProvider = PassRepoProvider;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AbstractSecretProvider = void 0;
|
|
4
|
+
class AbstractSecretProvider {
|
|
5
|
+
environment;
|
|
6
|
+
secretSource;
|
|
7
|
+
constructor(environment, secretSource) {
|
|
8
|
+
this.environment = environment;
|
|
9
|
+
this.secretSource = secretSource;
|
|
10
|
+
}
|
|
11
|
+
async get(_secretConfig) {
|
|
12
|
+
throw new Error('Not implemented');
|
|
13
|
+
}
|
|
14
|
+
async set(_secretConfig, _secretValue) {
|
|
15
|
+
throw new Error('Not implemented');
|
|
16
|
+
}
|
|
17
|
+
async list(_serviceName) {
|
|
18
|
+
throw new Error('Not implemented');
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
exports.AbstractSecretProvider = AbstractSecretProvider;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SSMProvider = void 0;
|
|
4
|
+
const client_ssm_1 = require("@aws-sdk/client-ssm");
|
|
5
|
+
const providers_types_1 = require("./providers.types");
|
|
6
|
+
const service_secrets_1 = require("../config/service_secrets");
|
|
7
|
+
const CONCOURSE_SECRETS = [
|
|
8
|
+
'cd-pay-dev',
|
|
9
|
+
'cd-pay-deploy',
|
|
10
|
+
'cd-main'
|
|
11
|
+
];
|
|
12
|
+
const CONCOURSE_PARAMETER_NAME_PREFIX = '/pay-cd/concourse/pipelines/';
|
|
13
|
+
const LOWERCASE_SECRET_NAMES_TO_SECRET_NAMES = service_secrets_1.SECRET_NAMES.reduce((result, secretName) => {
|
|
14
|
+
return { ...result, [secretName.toLowerCase()]: secretName };
|
|
15
|
+
}, {});
|
|
16
|
+
class SSMProvider extends providers_types_1.AbstractSecretProvider {
|
|
17
|
+
ssmClient;
|
|
18
|
+
constructor(env, secretSource) {
|
|
19
|
+
super(env, secretSource);
|
|
20
|
+
this.ssmClient = new client_ssm_1.SSMClient({ region: 'eu-west-1' });
|
|
21
|
+
}
|
|
22
|
+
async get(secretConfig) {
|
|
23
|
+
const ssmParameterName = ssmParameterNameForSecret(secretConfig);
|
|
24
|
+
const getParameterCommand = new client_ssm_1.GetParameterCommand({
|
|
25
|
+
Name: ssmParameterName,
|
|
26
|
+
WithDecryption: true
|
|
27
|
+
});
|
|
28
|
+
let response;
|
|
29
|
+
try {
|
|
30
|
+
response = await this.ssmClient.send(getParameterCommand);
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
if (e instanceof client_ssm_1.ParameterNotFound) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
else if (e instanceof Error && e.name === 'CredentialsProviderError') {
|
|
37
|
+
console.error('AWS Could not load any credentials, perhaps you need to run this with aws-vault: `aws-vault exec <account> -- pay secrets ...`?');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
console.error(`An error occured while trying to get the parameter ${ssmParameterName}.`);
|
|
41
|
+
throw e;
|
|
42
|
+
}
|
|
43
|
+
if (response.Parameter?.Value === undefined) {
|
|
44
|
+
let errorMessage = `When calling SSM to get the parameter ${ssmParameterName} there was no parameter or value returned, and no error raised! `;
|
|
45
|
+
if (response.$metadata.httpStatusCode === undefined) {
|
|
46
|
+
errorMessage += 'There was also no http status code in the response metadata!';
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
errorMessage += `The last http status code of the request to get the parameter was ${response.$metadata.httpStatusCode}`;
|
|
50
|
+
}
|
|
51
|
+
throw new Error(errorMessage);
|
|
52
|
+
}
|
|
53
|
+
return response.Parameter.Value;
|
|
54
|
+
}
|
|
55
|
+
async set(secretConfig, value) {
|
|
56
|
+
const ssmParameterName = ssmParameterNameForSecret(secretConfig);
|
|
57
|
+
const putParameterCommand = new client_ssm_1.PutParameterCommand({
|
|
58
|
+
Name: ssmParameterName,
|
|
59
|
+
Type: 'SecureString',
|
|
60
|
+
Value: value,
|
|
61
|
+
Overwrite: true
|
|
62
|
+
});
|
|
63
|
+
try {
|
|
64
|
+
await this.ssmClient.send(putParameterCommand);
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
if (e instanceof client_ssm_1.InvalidAllowedPatternException || e instanceof client_ssm_1.ParameterPatternMismatchException) {
|
|
68
|
+
console.error(`When trying to set secret ${secretConfig.name} in environment ${secretConfig.environment} for service ${secretConfig.service}`);
|
|
69
|
+
console.error(`AWS returned an error that the parameter ${ssmParameterName} is not a valid parameter name.`);
|
|
70
|
+
console.error('See https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-paramstore-su-create.html#sysman-parameter-name-constraints for constraints on parameter names');
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
else if (e instanceof Error && e.name === 'CredentialsProviderError') {
|
|
74
|
+
console.error('AWS Could not load any credentials, perhaps you need to run this with aws-vault: `aws-vault exec <account> -- pay secrets ...`?');
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
console.error(`When trying to set secret ${secretConfig.name} in environment ${secretConfig.environment} for service ${secretConfig.service} AWS returned an unhandled (by us) excpetion, re-throwing error`);
|
|
79
|
+
throw e;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
async list(serviceName) {
|
|
85
|
+
const filters = [
|
|
86
|
+
{
|
|
87
|
+
Key: 'Name',
|
|
88
|
+
Option: 'BeginsWith',
|
|
89
|
+
Values: [
|
|
90
|
+
ssmParameterNamePrefix(this.environment, serviceName)
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
];
|
|
94
|
+
const paginator = (0, client_ssm_1.paginateDescribeParameters)({ client: this.ssmClient }, { ParameterFilters: filters });
|
|
95
|
+
const parameters = [];
|
|
96
|
+
for await (const page of paginator) {
|
|
97
|
+
if (page.Parameters === undefined) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
parameters.push(...page.Parameters);
|
|
101
|
+
}
|
|
102
|
+
const secretConfigs = [];
|
|
103
|
+
for (const parameter of parameters) {
|
|
104
|
+
if (parameter.Name === undefined) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const secretConfig = this.ssmParameterNameToLowercaseSecret(parameter.Name, serviceName);
|
|
108
|
+
secretConfigs.push(secretConfig);
|
|
109
|
+
}
|
|
110
|
+
return secretConfigs;
|
|
111
|
+
}
|
|
112
|
+
ssmParameterNameToLowercaseSecret(parameterName, serviceName) {
|
|
113
|
+
if (parameterName.startsWith(CONCOURSE_PARAMETER_NAME_PREFIX)) {
|
|
114
|
+
return this.concourseParameterNameToLowercaseSecret(parameterName, serviceName);
|
|
115
|
+
}
|
|
116
|
+
const lowercaseSecretName = parameterName.substring(parameterName.indexOf('.') + 1);
|
|
117
|
+
return {
|
|
118
|
+
environment: this.environment,
|
|
119
|
+
name: lowercaseSecretName in LOWERCASE_SECRET_NAMES_TO_SECRET_NAMES ? LOWERCASE_SECRET_NAMES_TO_SECRET_NAMES[lowercaseSecretName] : lowercaseSecretName,
|
|
120
|
+
secretSourceValue: parameterName,
|
|
121
|
+
service: serviceName,
|
|
122
|
+
source: 'ssm'
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
concourseParameterNameToLowercaseSecret(parameterName, serviceName) {
|
|
126
|
+
const paramNameWithoutPrefix = parameterName.substring(CONCOURSE_PARAMETER_NAME_PREFIX.length);
|
|
127
|
+
const lowercaseSecretName = paramNameWithoutPrefix.substring(paramNameWithoutPrefix.indexOf('/') + 1);
|
|
128
|
+
return {
|
|
129
|
+
environment: this.environment,
|
|
130
|
+
name: lowercaseSecretName in LOWERCASE_SECRET_NAMES_TO_SECRET_NAMES ? LOWERCASE_SECRET_NAMES_TO_SECRET_NAMES[lowercaseSecretName] : lowercaseSecretName,
|
|
131
|
+
secretSourceValue: parameterName,
|
|
132
|
+
service: serviceName,
|
|
133
|
+
source: 'ssm'
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
exports.SSMProvider = SSMProvider;
|
|
138
|
+
function ssmParameterNameForSecret(secretConfig) {
|
|
139
|
+
if (CONCOURSE_SECRETS.includes(secretConfig.service)) {
|
|
140
|
+
return concourseSecretName(secretConfig);
|
|
141
|
+
}
|
|
142
|
+
return `${secretConfig.environment}_${secretConfig.service}.${secretConfig.name}`.toLowerCase();
|
|
143
|
+
}
|
|
144
|
+
function concourseSecretName(secretConfig) {
|
|
145
|
+
// Service name is `cd-pay-dev` but the ssm name is only `pay-dev` so remove the cd-
|
|
146
|
+
const serviceName = secretConfig.service.substring(3);
|
|
147
|
+
return `${CONCOURSE_PARAMETER_NAME_PREFIX}${serviceName.toLowerCase()}/${secretConfig.name.toLowerCase()}`;
|
|
148
|
+
}
|
|
149
|
+
function ssmParameterNamePrefix(environment, serviceName) {
|
|
150
|
+
if (CONCOURSE_SECRETS.includes(serviceName)) {
|
|
151
|
+
// Service name is `cd-pay-dev` but the ssm name is only `pay-dev` so remove the cd-
|
|
152
|
+
return `${CONCOURSE_PARAMETER_NAME_PREFIX}${serviceName.toLowerCase().substring(3)}/`;
|
|
153
|
+
}
|
|
154
|
+
return `${environment}_${serviceName}.`;
|
|
155
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ValueProvider = void 0;
|
|
4
|
+
const providers_types_1 = require("../providers/providers.types");
|
|
5
|
+
class ValueProvider extends providers_types_1.AbstractSecretProvider {
|
|
6
|
+
async get(secretConfig) {
|
|
7
|
+
return secretConfig.secretSourceValue;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
exports.ValueProvider = ValueProvider;
|
|
@@ -4,27 +4,59 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.handler = exports.builder = exports.desc = exports.command = void 0;
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
const cli_table3_1 = __importDefault(require("cli-table3"));
|
|
8
|
+
const config_types_1 = require("../config/config.types");
|
|
9
|
+
const service_secrets_js_1 = require("../config/service_secrets.js");
|
|
10
|
+
const ssm_1 = require("../providers/ssm");
|
|
11
|
+
const configs_1 = require("../../../util/configs");
|
|
12
|
+
const standardContent_js_1 = require("../../../core/standardContent.js");
|
|
13
|
+
exports.command = 'audit <service> <env>';
|
|
14
|
+
exports.desc = 'Check whether the configured secrets for <service> in <env> are provisioned. Also lists secrets which are provisioned but which we do not have config for. Note: Does not check the value of the secret, only existance';
|
|
11
15
|
const builder = (yargs) => {
|
|
12
16
|
return yargs
|
|
13
17
|
.positional('service', {
|
|
14
18
|
type: 'string',
|
|
15
|
-
description: 'The service (e.g. connector) to audit the secret for'
|
|
19
|
+
description: 'The service (e.g. connector) to audit the secret for',
|
|
20
|
+
choices: config_types_1.SERVICE_NAMES
|
|
16
21
|
})
|
|
17
22
|
.positional('env', {
|
|
18
23
|
type: 'string',
|
|
19
|
-
description: 'The environment (e.g. test-12) to audit the secret for'
|
|
24
|
+
description: 'The environment (e.g. test-12) to audit the secret for',
|
|
25
|
+
choices: config_types_1.ENVIRONMENT_NAMES
|
|
20
26
|
});
|
|
21
27
|
};
|
|
22
28
|
exports.builder = builder;
|
|
23
29
|
exports.handler = auditHandler;
|
|
24
30
|
async function auditHandler(argv) {
|
|
25
31
|
const service = argv.service;
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
(0,
|
|
32
|
+
const env = argv.env;
|
|
33
|
+
await (0, standardContent_js_1.showHeader)();
|
|
34
|
+
await (0, configs_1.checkAwsCredentials)((0, configs_1.payEnvironmentToAWSAccountName)(env));
|
|
35
|
+
const secretsToAudit = service_secrets_js_1.SERVICE_SECRETS[service].sort();
|
|
36
|
+
const ssmProvider = new ssm_1.SSMProvider(env, 'ssm');
|
|
37
|
+
const secretsInSSM = await ssmProvider.list(service);
|
|
38
|
+
const checkedSecretsInSSM = new Map();
|
|
39
|
+
for (const secretInSSM of secretsInSSM) {
|
|
40
|
+
checkedSecretsInSSM.set(secretInSSM.name, secretInSSM);
|
|
41
|
+
}
|
|
42
|
+
const table = new cli_table3_1.default({
|
|
43
|
+
head: [`Secrets for ${service}`, env]
|
|
44
|
+
});
|
|
45
|
+
for (const secretToAudit of secretsToAudit) {
|
|
46
|
+
if (checkedSecretsInSSM.has(secretToAudit)) {
|
|
47
|
+
table.push([secretToAudit, '✅']);
|
|
48
|
+
checkedSecretsInSSM.delete(secretToAudit);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
table.push([secretToAudit, '❌']);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (checkedSecretsInSSM.size > 0) {
|
|
55
|
+
table.push([
|
|
56
|
+
'Provisioned Secrets which are not configured in pay secrets',
|
|
57
|
+
Array.from(checkedSecretsInSSM.keys()).sort().join('\n')
|
|
58
|
+
]);
|
|
59
|
+
}
|
|
60
|
+
console.log(table.toString());
|
|
29
61
|
}
|
|
30
62
|
exports.default = auditHandler;
|
|
@@ -1,26 +1,30 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.handler = exports.builder = exports.desc = exports.command = void 0;
|
|
7
|
-
const
|
|
8
|
-
const
|
|
4
|
+
const config_types_1 = require("../config/config.types");
|
|
5
|
+
const service_secrets_1 = require("../config/service_secrets");
|
|
6
|
+
const secrets_1 = require("../config/secrets");
|
|
7
|
+
const configs_1 = require("../../../util/configs");
|
|
8
|
+
const factory_1 = require("../providers/factory");
|
|
9
|
+
const standardContent_1 = require("../../../core/standardContent");
|
|
9
10
|
exports.command = 'fetch <env> <service> <secret_name> [--use-ssm]';
|
|
10
11
|
exports.desc = 'Fetches a <named secret> for <service> for <env>, if use-ssm is set then get the secret from ssm';
|
|
11
12
|
const builder = (yargs) => {
|
|
12
13
|
return yargs
|
|
13
14
|
.positional('env', {
|
|
14
15
|
type: 'string',
|
|
15
|
-
description: 'The environment (e.g. test-12) to fetch the secret for'
|
|
16
|
+
description: 'The environment (e.g. test-12) to fetch the secret for',
|
|
17
|
+
choices: config_types_1.ENVIRONMENT_NAMES
|
|
16
18
|
})
|
|
17
19
|
.positional('service', {
|
|
18
20
|
type: 'string',
|
|
19
|
-
description: 'The service (e.g. connector) to fetch the secret for'
|
|
21
|
+
description: 'The service (e.g. connector) to fetch the secret for',
|
|
22
|
+
choices: config_types_1.SERVICE_NAMES
|
|
20
23
|
})
|
|
21
24
|
.positional('secret_name', {
|
|
22
25
|
type: 'string',
|
|
23
|
-
description: 'The name of the secret to get'
|
|
26
|
+
description: 'The name of the secret to get',
|
|
27
|
+
choices: service_secrets_1.SECRET_NAMES
|
|
24
28
|
})
|
|
25
29
|
.option('use-ssm', {
|
|
26
30
|
type: 'boolean',
|
|
@@ -31,17 +35,34 @@ const builder = (yargs) => {
|
|
|
31
35
|
exports.builder = builder;
|
|
32
36
|
exports.handler = fetchHandler;
|
|
33
37
|
async function fetchHandler(argv) {
|
|
34
|
-
const service = argv.service;
|
|
35
38
|
const env = argv.env;
|
|
39
|
+
const service = argv.service;
|
|
36
40
|
const secretName = argv.secret_name;
|
|
37
41
|
const useSSM = argv.useSsm;
|
|
38
|
-
|
|
39
|
-
|
|
42
|
+
// Printing the header to stdout so you can use this command piped to another process
|
|
43
|
+
// and the other process will only recevie the printed secret. E.g | pbcopy to copy the secret
|
|
44
|
+
// to your copy paste buffer without the header etc
|
|
45
|
+
await (0, standardContent_1.showHeaderStdErr)();
|
|
46
|
+
const secretConfig = (0, secrets_1.getSecretConfig)(env, service, secretName);
|
|
40
47
|
if (useSSM) {
|
|
41
|
-
|
|
48
|
+
secretConfig.source = 'ssm';
|
|
49
|
+
}
|
|
50
|
+
if (secretConfig.source === 'ssm') {
|
|
51
|
+
await (0, configs_1.checkAwsCredentials)((0, configs_1.payEnvironmentToAWSAccountName)(env));
|
|
52
|
+
}
|
|
53
|
+
const secretProvider = (0, factory_1.providerFor)(secretConfig);
|
|
54
|
+
const secretValue = await secretProvider.get(secretConfig);
|
|
55
|
+
if (secretValue === undefined) {
|
|
56
|
+
console.error(`The secret ${secretName} was not found in ${secretConfig.source}`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
if (!process.stdout.isTTY) {
|
|
60
|
+
// If stdout of the process isn't connected to a terminal (e.g. if it's piped to another process, like pbcopy for instance)
|
|
61
|
+
// Print to stderr a message to make it clear to the user the value is being hidden from display, but has been printed to stdout
|
|
62
|
+
process.stderr.write('HIDDEN - Actual Value written to stdout');
|
|
42
63
|
}
|
|
43
|
-
|
|
44
|
-
//
|
|
45
|
-
console.
|
|
64
|
+
process.stdout.write(secretValue);
|
|
65
|
+
// Since the secret is written with no trailing newline we should print one (to stderr) to improve the UX
|
|
66
|
+
console.warn();
|
|
46
67
|
}
|
|
47
68
|
exports.default = fetchHandler;
|
|
@@ -4,19 +4,36 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.handler = exports.builder = exports.desc = exports.command = void 0;
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
|
|
7
|
+
const cli_table3_1 = __importDefault(require("cli-table3"));
|
|
8
|
+
const promises_1 = __importDefault(require("node:readline/promises"));
|
|
9
|
+
const config_types_1 = require("../config/config.types");
|
|
10
|
+
const secrets_1 = require("../config/secrets");
|
|
11
|
+
const configs_1 = require("../../../util/configs");
|
|
12
|
+
const factory_1 = require("../providers/factory");
|
|
13
|
+
const standardContent_js_1 = require("../../../core/standardContent.js");
|
|
14
|
+
exports.command = 'provision <service> <env> [--redact] [--dry-run]';
|
|
10
15
|
exports.desc = 'Provisions secrets from config for <service> in <env>';
|
|
11
16
|
const builder = (yargs) => {
|
|
12
17
|
return yargs
|
|
13
18
|
.positional('service', {
|
|
14
19
|
type: 'string',
|
|
15
|
-
description: 'The service (e.g. connector) to provision secrets for'
|
|
20
|
+
description: 'The service (e.g. connector) to provision secrets for',
|
|
21
|
+
choices: config_types_1.SERVICE_NAMES
|
|
16
22
|
})
|
|
17
23
|
.positional('env', {
|
|
18
24
|
type: 'string',
|
|
19
|
-
description: 'The environment (e.g. test-12) to provision the secrets in'
|
|
25
|
+
description: 'The environment (e.g. test-12) to provision the secrets in',
|
|
26
|
+
choices: config_types_1.ENVIRONMENT_NAMES
|
|
27
|
+
})
|
|
28
|
+
.option('redact', {
|
|
29
|
+
type: 'boolean',
|
|
30
|
+
default: false,
|
|
31
|
+
description: 'During a dry-run redact all printed secret values except for an empty string (which makes it suitable to paste into a PR)'
|
|
32
|
+
})
|
|
33
|
+
.option('dry-run', {
|
|
34
|
+
type: 'boolean',
|
|
35
|
+
default: false,
|
|
36
|
+
description: 'Perform a dry run, will not set any secrets, but will show all changes'
|
|
20
37
|
});
|
|
21
38
|
};
|
|
22
39
|
exports.builder = builder;
|
|
@@ -24,7 +41,82 @@ exports.handler = provisionHandler;
|
|
|
24
41
|
async function provisionHandler(argv) {
|
|
25
42
|
const service = argv.service;
|
|
26
43
|
const env = argv.env;
|
|
27
|
-
const
|
|
28
|
-
|
|
44
|
+
const redact = argv.redact;
|
|
45
|
+
const dryRun = argv.dryRun;
|
|
46
|
+
await (0, standardContent_js_1.showHeader)();
|
|
47
|
+
if (redact && !dryRun) {
|
|
48
|
+
console.error('The --redact option is only valid with the --dry-run option also specified');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
await (0, configs_1.checkAwsCredentials)((0, configs_1.payEnvironmentToAWSAccountName)(env));
|
|
52
|
+
const rl = promises_1.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
53
|
+
const secretsToProvision = [];
|
|
54
|
+
for (const sourceSecret of (0, secrets_1.configuredSecretsForServiceInEnv)(env, service)) {
|
|
55
|
+
secretsToProvision.push({
|
|
56
|
+
source: sourceSecret,
|
|
57
|
+
destination: {
|
|
58
|
+
...sourceSecret,
|
|
59
|
+
source: 'ssm' // Currently all secrets are provisioned to ssm, but that could change in the future
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
for (const secretToProvision of secretsToProvision) {
|
|
64
|
+
const sourceProvider = (0, factory_1.providerFor)(secretToProvision.source);
|
|
65
|
+
const destinationProvider = (0, factory_1.providerFor)(secretToProvision.destination);
|
|
66
|
+
const sourceValue = await sourceProvider.get(secretToProvision.source);
|
|
67
|
+
if (sourceValue === undefined) {
|
|
68
|
+
console.error(`Secret ${secretToProvision.source.name} does not exist in ${secretToProvision.source.source}`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
const destinationValue = await destinationProvider.get(secretToProvision.destination);
|
|
72
|
+
if (sourceValue === destinationValue) {
|
|
73
|
+
console.log(`✅ Provisioning ${secretToProvision.source.name} in ${secretToProvision.destination.source} in environment ${secretToProvision.source.environment} for ${secretToProvision.source.service} would apply no change`);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (destinationValue === undefined) {
|
|
77
|
+
console.log(`❓ Secret ${secretToProvision.source.name} does not exist in ${secretToProvision.destination.source} in environment ${secretToProvision.source.environment} for ${secretToProvision.source.service}, provision?`);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
console.log(`❓ Update ${secretToProvision.source.name} in ${secretToProvision.destination.source} in environment ${secretToProvision.source.environment} for ${secretToProvision.source.service}?`);
|
|
81
|
+
}
|
|
82
|
+
const table = new cli_table3_1.default({
|
|
83
|
+
head: ['Old value', 'New Value']
|
|
84
|
+
});
|
|
85
|
+
table.push([
|
|
86
|
+
secretForDisplay(destinationValue, redact),
|
|
87
|
+
secretForDisplay(sourceValue, redact)
|
|
88
|
+
]);
|
|
89
|
+
console.log(table.toString());
|
|
90
|
+
if (dryRun) {
|
|
91
|
+
console.log('Dry run only....not provisioning');
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const confirmResponse = await rl.question(' Type "yes" exactly > ');
|
|
95
|
+
if (confirmResponse.trim() !== 'yes') {
|
|
96
|
+
console.log('User rejected confirmation, secret NOT provisioned');
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (await destinationProvider.set(secretToProvision.destination, sourceValue)) {
|
|
100
|
+
console.log('✅ Secret successfully provisioned');
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
console.error('❌ Saving the secret failed for an unknown reason');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
console.log('\nProvision finished');
|
|
107
|
+
rl.close();
|
|
29
108
|
}
|
|
30
109
|
exports.default = provisionHandler;
|
|
110
|
+
function secretForDisplay(secretValue, redact) {
|
|
111
|
+
if (secretValue === undefined) {
|
|
112
|
+
return '👻 NOT CURRENTLY SET 👻';
|
|
113
|
+
}
|
|
114
|
+
// Purposefully not redacting if the secret is an empty string
|
|
115
|
+
if (secretValue.trim() === '') {
|
|
116
|
+
return '"" (❗EMPTY STRING❗)';
|
|
117
|
+
}
|
|
118
|
+
if (redact) {
|
|
119
|
+
return '🤫 REDACTED 🤫';
|
|
120
|
+
}
|
|
121
|
+
return secretValue;
|
|
122
|
+
}
|
package/src/commands/secrets.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.builder = exports.desc = exports.command = void 0;
|
|
4
4
|
exports.command = 'secrets';
|
|
5
|
-
exports.desc = 'Provision secrets
|
|
5
|
+
exports.desc = 'Provision secrets';
|
|
6
6
|
const builder = (yargs) => {
|
|
7
7
|
return yargs
|
|
8
8
|
.commandDir('secrets/subcommands')
|
|
@@ -18,6 +18,7 @@ async function runCommand() {
|
|
|
18
18
|
.strict()
|
|
19
19
|
.help()
|
|
20
20
|
.wrap(yargsInstance.terminalWidth())
|
|
21
|
+
.completion('completion', 'Generate shell (bash/zsh only) auto-completion script. Put the script at the end of your .bashrc or .zshrc')
|
|
21
22
|
.parse();
|
|
22
23
|
}
|
|
23
24
|
exports.default = runCommand;
|
|
@@ -23,7 +23,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
|
23
23
|
return result;
|
|
24
24
|
};
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
-
exports.showHeader = void 0;
|
|
26
|
+
exports.showHeaderStdErr = exports.showHeader = void 0;
|
|
27
27
|
const fsp = __importStar(require("fs/promises"));
|
|
28
28
|
const path = __importStar(require("path"));
|
|
29
29
|
const constants_js_1 = require("./constants.js");
|
|
@@ -31,3 +31,7 @@ async function showHeader() {
|
|
|
31
31
|
console.log(await fsp.readFile(path.join(constants_js_1.rootDir, 'resources/header.txt'), 'utf8'));
|
|
32
32
|
}
|
|
33
33
|
exports.showHeader = showHeader;
|
|
34
|
+
async function showHeaderStdErr() {
|
|
35
|
+
console.warn(await fsp.readFile(path.join(constants_js_1.rootDir, 'resources/header.txt'), 'utf8'));
|
|
36
|
+
}
|
|
37
|
+
exports.showHeaderStdErr = showHeaderStdErr;
|
package/src/util/configs.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.checkAwsCredentials = exports.workspaceEnvVar = exports.ensureConfigDirectory = exports.PAY_CLI_CONFIG_PATH = void 0;
|
|
6
|
+
exports.payEnvironmentToAWSAccountName = exports.checkAwsCredentials = exports.workspaceEnvVar = exports.ensureConfigDirectory = exports.PAY_CLI_CONFIG_PATH = void 0;
|
|
7
7
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
8
|
const node_os_1 = require("node:os");
|
|
9
9
|
const node_path_1 = __importDefault(require("node:path"));
|
|
@@ -106,3 +106,9 @@ async function checkAwsCredentialsAreForAccount(accountName) {
|
|
|
106
106
|
process.exit(1);
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
|
+
/* This function will turn a Pay environment name into an AWS account name
|
|
110
|
+
*/
|
|
111
|
+
function payEnvironmentToAWSAccountName(environmentName) {
|
|
112
|
+
return environmentName.split('-')[0];
|
|
113
|
+
}
|
|
114
|
+
exports.payEnvironmentToAWSAccountName = payEnvironmentToAWSAccountName;
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.handler = exports.builder = exports.desc = exports.command = void 0;
|
|
7
|
-
const node_child_process_1 = require("node:child_process");
|
|
8
|
-
const preflight_js_1 = __importDefault(require("../utils/preflight.js"));
|
|
9
|
-
exports.command = 'copy <service> <src_env> <dest_env>';
|
|
10
|
-
exports.desc = 'Copies secrets for <service> from <src_env> to <dest_env>';
|
|
11
|
-
const builder = (yargs) => {
|
|
12
|
-
return yargs
|
|
13
|
-
.positional('service', {
|
|
14
|
-
type: 'string',
|
|
15
|
-
description: 'The service (e.g. connector) to copy the secrets for'
|
|
16
|
-
})
|
|
17
|
-
.positional('src_env', {
|
|
18
|
-
type: 'string',
|
|
19
|
-
description: 'The environment (e.g. test-12) to copy the secrets from'
|
|
20
|
-
})
|
|
21
|
-
.positional('dest_env', {
|
|
22
|
-
type: 'string',
|
|
23
|
-
description: 'The environment (e.g. test-perf-1) to copy the secrets to'
|
|
24
|
-
});
|
|
25
|
-
};
|
|
26
|
-
exports.builder = builder;
|
|
27
|
-
exports.handler = copyHandler;
|
|
28
|
-
async function copyHandler(argv) {
|
|
29
|
-
const service = argv.service;
|
|
30
|
-
const srcEnv = argv.src_env;
|
|
31
|
-
const destEnv = argv.dest_env;
|
|
32
|
-
const preflightInfo = (0, preflight_js_1.default)();
|
|
33
|
-
(0, node_child_process_1.spawnSync)(preflightInfo.rbenvCommand, ['exec', 'bundle', 'exec', 'bin/pay', 'secrets', 'copy', service, srcEnv, destEnv], { shell: true, stdio: 'inherit', cwd: preflightInfo.pathToLegacyRubyCli });
|
|
34
|
-
}
|
|
35
|
-
exports.default = copyHandler;
|