@beesolve/aws-accounts 1.0.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.
- package/LICENSE +21 -0
- package/README.md +189 -0
- package/dist/accountCreation.js +135 -0
- package/dist/applyLogic.js +1203 -0
- package/dist/awsClientConfig.js +26 -0
- package/dist/awsConfig.js +1365 -0
- package/dist/cli.js +201 -0
- package/dist/commands/graveyard.js +46 -0
- package/dist/commands/regenerate.js +17 -0
- package/dist/commands/remote.js +925 -0
- package/dist/diff.js +1012 -0
- package/dist/error.js +66 -0
- package/dist/helpers.js +21 -0
- package/dist/lambda/handler.js +375 -0
- package/dist/lambdaClient.js +220 -0
- package/dist/logger.js +26 -0
- package/dist/operations.js +218 -0
- package/dist/remoteStateCache.js +38 -0
- package/dist/reservedOuDeletion.js +46 -0
- package/dist/scanLogic.js +456 -0
- package/dist/state.js +618 -0
- package/dist/tags.js +14 -0
- package/dist-lambda/handler.mjs +3558 -0
- package/dist-lambda/lambda.zip +0 -0
- package/package.json +59 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
|
+
import { S3Client } from "@aws-sdk/client-s3";
|
|
4
|
+
import { IAMClient } from "@aws-sdk/client-iam";
|
|
5
|
+
import { LambdaClient } from "@aws-sdk/client-lambda";
|
|
6
|
+
import { STSClient } from "@aws-sdk/client-sts";
|
|
7
|
+
import { SSOAdminClient } from "@aws-sdk/client-sso-admin";
|
|
8
|
+
import {
|
|
9
|
+
buildAwsClientConfig,
|
|
10
|
+
resolveAwsProfile,
|
|
11
|
+
resolveAwsRegion
|
|
12
|
+
} from "./awsClientConfig.js";
|
|
13
|
+
import { consoleLogger } from "./logger.js";
|
|
14
|
+
import { runGraveyardCommand } from "./commands/graveyard.js";
|
|
15
|
+
import { runRegenerateCommand } from "./commands/regenerate.js";
|
|
16
|
+
import {
|
|
17
|
+
runRemoteBootstrap,
|
|
18
|
+
runRemoteScan,
|
|
19
|
+
runRemoteInit,
|
|
20
|
+
runRemotePlan,
|
|
21
|
+
runRemoteApply,
|
|
22
|
+
runRemoteUpgrade
|
|
23
|
+
} from "./commands/remote.js";
|
|
24
|
+
import {
|
|
25
|
+
classifyCliError,
|
|
26
|
+
exitCodeForCliErrorKind,
|
|
27
|
+
toUsageError
|
|
28
|
+
} from "./error.js";
|
|
29
|
+
const commands = [
|
|
30
|
+
"bootstrap",
|
|
31
|
+
"scan",
|
|
32
|
+
"init",
|
|
33
|
+
"regenerate",
|
|
34
|
+
"graveyard",
|
|
35
|
+
"plan",
|
|
36
|
+
"apply",
|
|
37
|
+
"upgrade"
|
|
38
|
+
];
|
|
39
|
+
function isCommandName(value) {
|
|
40
|
+
return commands.includes(value);
|
|
41
|
+
}
|
|
42
|
+
const contextPath = "aws.context.json";
|
|
43
|
+
async function main() {
|
|
44
|
+
const logger = consoleLogger;
|
|
45
|
+
const args = parseArgs({
|
|
46
|
+
options: {
|
|
47
|
+
profile: { type: "string" },
|
|
48
|
+
region: { type: "string" },
|
|
49
|
+
yes: { type: "boolean", default: false },
|
|
50
|
+
json: { type: "boolean", default: false },
|
|
51
|
+
"ignore-unsupported": { type: "boolean", default: false },
|
|
52
|
+
"allow-destructive": { type: "boolean", default: false },
|
|
53
|
+
refresh: { type: "boolean", default: false },
|
|
54
|
+
help: { type: "boolean", default: false }
|
|
55
|
+
},
|
|
56
|
+
allowPositionals: true
|
|
57
|
+
});
|
|
58
|
+
const commandArg = args.positionals[0];
|
|
59
|
+
if (args.values.help || commandArg == null) {
|
|
60
|
+
printHelp(logger);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (!isCommandName(commandArg)) {
|
|
64
|
+
printHelp(logger);
|
|
65
|
+
throw toUsageError(`Unknown command: "${commandArg}".`);
|
|
66
|
+
}
|
|
67
|
+
const command = commandArg;
|
|
68
|
+
const profile = resolveAwsProfile({ profileArg: args.values.profile });
|
|
69
|
+
const region = resolveAwsRegion({ regionArg: args.values.region });
|
|
70
|
+
const clientConfig = buildAwsClientConfig({
|
|
71
|
+
profile,
|
|
72
|
+
region
|
|
73
|
+
});
|
|
74
|
+
if (command === "regenerate") {
|
|
75
|
+
const overwriteConfirmation2 = buildOverwriteConfirmation({
|
|
76
|
+
yes: args.values.yes ?? false,
|
|
77
|
+
isTty: process.stdin.isTTY
|
|
78
|
+
});
|
|
79
|
+
const result = await runRegenerateCommand({
|
|
80
|
+
logger,
|
|
81
|
+
overwriteConfirmation: overwriteConfirmation2
|
|
82
|
+
});
|
|
83
|
+
logger.log("");
|
|
84
|
+
logger.log("Regenerate complete.");
|
|
85
|
+
for (const file of result.files) {
|
|
86
|
+
logger.log(`${file.path}: ${file.status}`);
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (command === "graveyard") {
|
|
91
|
+
await runGraveyardCommand({
|
|
92
|
+
logger,
|
|
93
|
+
cachePath: ".remote-state-cache.json",
|
|
94
|
+
contextPath
|
|
95
|
+
});
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const overwriteConfirmation = buildOverwriteConfirmation({
|
|
99
|
+
yes: args.values.yes ?? false,
|
|
100
|
+
isTty: process.stdin.isTTY
|
|
101
|
+
});
|
|
102
|
+
const remoteInput = {
|
|
103
|
+
subcommand: command,
|
|
104
|
+
profile,
|
|
105
|
+
region,
|
|
106
|
+
flags: {
|
|
107
|
+
yes: args.values.yes ?? false,
|
|
108
|
+
refresh: args.values.refresh ?? false,
|
|
109
|
+
allowDestructive: args.values["allow-destructive"] ?? false,
|
|
110
|
+
ignoreUnsupported: args.values["ignore-unsupported"] ?? false
|
|
111
|
+
},
|
|
112
|
+
logger,
|
|
113
|
+
overwriteConfirmation,
|
|
114
|
+
stsClient: new STSClient(clientConfig),
|
|
115
|
+
s3Client: new S3Client(clientConfig),
|
|
116
|
+
iamClient: new IAMClient(clientConfig),
|
|
117
|
+
lambdaClient: new LambdaClient(clientConfig),
|
|
118
|
+
ssoAdminClient: new SSOAdminClient(clientConfig)
|
|
119
|
+
};
|
|
120
|
+
if (command === "bootstrap") {
|
|
121
|
+
return runRemoteBootstrap(remoteInput);
|
|
122
|
+
}
|
|
123
|
+
if (command === "scan") {
|
|
124
|
+
return runRemoteScan(remoteInput);
|
|
125
|
+
}
|
|
126
|
+
if (command === "init") {
|
|
127
|
+
return runRemoteInit(remoteInput);
|
|
128
|
+
}
|
|
129
|
+
if (command === "plan") {
|
|
130
|
+
return runRemotePlan(remoteInput);
|
|
131
|
+
}
|
|
132
|
+
if (command === "apply") {
|
|
133
|
+
return runRemoteApply(remoteInput);
|
|
134
|
+
}
|
|
135
|
+
if (command === "upgrade") {
|
|
136
|
+
return runRemoteUpgrade(remoteInput);
|
|
137
|
+
}
|
|
138
|
+
printHelp(logger);
|
|
139
|
+
process.exitCode = 1;
|
|
140
|
+
}
|
|
141
|
+
function printHelp(logger) {
|
|
142
|
+
logger.log("@beesolve/aws-accounts");
|
|
143
|
+
logger.log("");
|
|
144
|
+
logger.log("Usage:");
|
|
145
|
+
logger.log(
|
|
146
|
+
" npm run cli -- bootstrap [--profile <name>] [--region <region>] [--yes]"
|
|
147
|
+
);
|
|
148
|
+
logger.log(
|
|
149
|
+
" npm run cli -- scan [--profile <name>] [--region <region>]"
|
|
150
|
+
);
|
|
151
|
+
logger.log(
|
|
152
|
+
" npm run cli -- init [--profile <name>] [--region <region>] [--yes]"
|
|
153
|
+
);
|
|
154
|
+
logger.log(" npm run cli -- regenerate [--yes]");
|
|
155
|
+
logger.log(" npm run cli -- graveyard");
|
|
156
|
+
logger.log(
|
|
157
|
+
" npm run cli -- plan [--profile <name>] [--region <region>] [--refresh]"
|
|
158
|
+
);
|
|
159
|
+
logger.log(
|
|
160
|
+
" npm run cli -- apply [--profile <name>] [--region <region>] [--yes] [--allow-destructive] [--ignore-unsupported]"
|
|
161
|
+
);
|
|
162
|
+
logger.log(
|
|
163
|
+
" npm run cli -- upgrade [--profile <name>] [--region <region>]"
|
|
164
|
+
);
|
|
165
|
+
logger.log("");
|
|
166
|
+
logger.log("Environment fallback:");
|
|
167
|
+
logger.log(" AWS_PROFILE, AWS_REGION, AWS_DEFAULT_REGION");
|
|
168
|
+
}
|
|
169
|
+
function buildOverwriteConfirmation(props) {
|
|
170
|
+
return async (overwriteProps) => {
|
|
171
|
+
if (overwriteProps.fileSummaries.length === 0) {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
if (props.yes) {
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
if (props.isTty !== true) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
"Refusing to overwrite config files in non-interactive mode without --yes."
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
const readlineInterface = createInterface({
|
|
183
|
+
input: process.stdin,
|
|
184
|
+
output: process.stdout
|
|
185
|
+
});
|
|
186
|
+
try {
|
|
187
|
+
const answer = await readlineInterface.question(
|
|
188
|
+
"Proceed with writing config files? [y/N] "
|
|
189
|
+
);
|
|
190
|
+
const normalized = answer.trim().toLowerCase();
|
|
191
|
+
return normalized === "y" || normalized === "yes";
|
|
192
|
+
} finally {
|
|
193
|
+
readlineInterface.close();
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
main().catch((error) => {
|
|
198
|
+
const classified = classifyCliError(error);
|
|
199
|
+
consoleLogger.error(`CLI ${classified.kind} error: ${classified.message}`);
|
|
200
|
+
process.exitCode = exitCodeForCliErrorKind(classified.kind);
|
|
201
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { readAwsContextFromFile } from "../awsConfig.js";
|
|
2
|
+
import { readStateCache } from "../remoteStateCache.js";
|
|
3
|
+
async function runGraveyardCommand(props) {
|
|
4
|
+
const [cache, context] = await Promise.all([
|
|
5
|
+
readStateCache(props.cachePath),
|
|
6
|
+
readAwsContextFromFile(props.contextPath)
|
|
7
|
+
]);
|
|
8
|
+
if (cache == null) {
|
|
9
|
+
throw new Error(
|
|
10
|
+
`No remote state cache found at "${props.cachePath}". Run a scan or apply command first to populate the cache.`
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
const state = cache.state;
|
|
14
|
+
const graveyardOuId = context.organization.graveyardOuId;
|
|
15
|
+
const graveyardAccounts = state.organization.accounts.filter((account) => account.parentId === graveyardOuId).sort((left, right) => left.name.localeCompare(right.name)).map((account) => ({
|
|
16
|
+
id: account.id,
|
|
17
|
+
name: account.name,
|
|
18
|
+
email: account.email,
|
|
19
|
+
status: account.status
|
|
20
|
+
}));
|
|
21
|
+
props.logger.log(`Graveyard OU: ${graveyardOuId}`);
|
|
22
|
+
props.logger.log(`Accounts in Graveyard: ${graveyardAccounts.length}`);
|
|
23
|
+
if (graveyardAccounts.length === 0) {
|
|
24
|
+
props.logger.log("No accounts currently parked in Graveyard.");
|
|
25
|
+
return {
|
|
26
|
+
graveyardOuId,
|
|
27
|
+
accounts: graveyardAccounts
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
props.logger.log("");
|
|
31
|
+
for (const account of graveyardAccounts) {
|
|
32
|
+
props.logger.log(
|
|
33
|
+
`- ${account.name} (${account.id}) [${account.status}] <${account.email}>`
|
|
34
|
+
);
|
|
35
|
+
props.logger.log(
|
|
36
|
+
` aws organizations close-account --account-id ${account.id}`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
graveyardOuId,
|
|
41
|
+
accounts: graveyardAccounts
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export {
|
|
45
|
+
runGraveyardCommand
|
|
46
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { regenerateAwsConfigTypes } from "../awsConfig.js";
|
|
2
|
+
async function runRegenerateCommand(props) {
|
|
3
|
+
const result = await regenerateAwsConfigTypes({
|
|
4
|
+
configPath: props.configPath ?? "aws.config.ts",
|
|
5
|
+
typesPath: props.typesPath ?? "aws.config.types.ts",
|
|
6
|
+
logger: props.logger,
|
|
7
|
+
overwriteConfirmation: props.overwriteConfirmation
|
|
8
|
+
});
|
|
9
|
+
return {
|
|
10
|
+
typesPath: result.typesPath,
|
|
11
|
+
changed: result.changed,
|
|
12
|
+
files: result.files
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export {
|
|
16
|
+
runRegenerateCommand
|
|
17
|
+
};
|