@beesolve/aws-accounts 1.1.0 → 1.2.1
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/README.md +1 -1
- package/dist/applyLogic.js +571 -225
- package/dist/awsConfig.js +441 -30
- package/dist/cli.js +86 -23
- package/dist/commands/graveyard.js +27 -0
- package/dist/commands/remote.js +206 -39
- package/dist/commands/validate.js +75 -0
- package/dist/diff.js +336 -14
- package/dist/lambda/handler.js +8 -4
- package/dist/lambdaClient.js +5 -2
- package/dist/operations.js +116 -1
- package/dist/scanLogic.js +237 -9
- package/dist/state.js +244 -8
- package/dist-lambda/handler.mjs +1164 -248
- package/dist-lambda/lambda.zip +0 -0
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
2
|
import { createInterface } from "node:readline/promises";
|
|
3
|
+
import { basename } from "node:path";
|
|
3
4
|
import { S3Client } from "@aws-sdk/client-s3";
|
|
4
5
|
import { IAMClient } from "@aws-sdk/client-iam";
|
|
5
6
|
import { LambdaClient } from "@aws-sdk/client-lambda";
|
|
@@ -11,7 +12,10 @@ import {
|
|
|
11
12
|
resolveAwsRegion
|
|
12
13
|
} from "./awsClientConfig.js";
|
|
13
14
|
import { consoleLogger } from "./logger.js";
|
|
14
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
runGraveyardCloseCommand,
|
|
17
|
+
runGraveyardCommand
|
|
18
|
+
} from "./commands/graveyard.js";
|
|
15
19
|
import { runProfileCommand } from "./commands/profile.js";
|
|
16
20
|
import { runRegenerateCommand } from "./commands/regenerate.js";
|
|
17
21
|
import { runValidateCommand } from "./commands/validate.js";
|
|
@@ -21,13 +25,16 @@ import {
|
|
|
21
25
|
runRemoteInit,
|
|
22
26
|
runRemotePlan,
|
|
23
27
|
runRemoteApply,
|
|
24
|
-
runRemoteUpgrade
|
|
28
|
+
runRemoteUpgrade,
|
|
29
|
+
runRemoteDrift
|
|
25
30
|
} from "./commands/remote.js";
|
|
26
31
|
import {
|
|
27
32
|
classifyCliError,
|
|
28
33
|
exitCodeForCliErrorKind,
|
|
29
34
|
toUsageError
|
|
30
35
|
} from "./error.js";
|
|
36
|
+
import { assertUnreachable } from "./helpers.js";
|
|
37
|
+
import { readAwsContextFromFile, readPackageVersion } from "./awsConfig.js";
|
|
31
38
|
const commands = [
|
|
32
39
|
"bootstrap",
|
|
33
40
|
"scan",
|
|
@@ -38,7 +45,8 @@ const commands = [
|
|
|
38
45
|
"profile",
|
|
39
46
|
"plan",
|
|
40
47
|
"apply",
|
|
41
|
-
"upgrade"
|
|
48
|
+
"upgrade",
|
|
49
|
+
"drift"
|
|
42
50
|
];
|
|
43
51
|
function isCommandName(value) {
|
|
44
52
|
return commands.includes(value);
|
|
@@ -55,6 +63,7 @@ async function main() {
|
|
|
55
63
|
"ignore-unsupported": { type: "boolean", default: false },
|
|
56
64
|
"allow-destructive": { type: "boolean", default: false },
|
|
57
65
|
refresh: { type: "boolean", default: false },
|
|
66
|
+
update: { type: "boolean", default: false },
|
|
58
67
|
"sso-start-url": { type: "string" },
|
|
59
68
|
"sso-session": { type: "string", default: "sso" },
|
|
60
69
|
help: { type: "boolean", default: false }
|
|
@@ -101,6 +110,21 @@ async function main() {
|
|
|
101
110
|
return;
|
|
102
111
|
}
|
|
103
112
|
if (command === "graveyard") {
|
|
113
|
+
const subcommand = args.positionals[1];
|
|
114
|
+
if (subcommand === "close") {
|
|
115
|
+
await runGraveyardCloseCommand({
|
|
116
|
+
logger,
|
|
117
|
+
cachePath: ".remote-state-cache.json",
|
|
118
|
+
contextPath
|
|
119
|
+
});
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (subcommand != null) {
|
|
123
|
+
printHelp(logger);
|
|
124
|
+
throw toUsageError(
|
|
125
|
+
`Unknown graveyard subcommand: "${subcommand}". Valid subcommands: close`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
104
128
|
await runGraveyardCommand({
|
|
105
129
|
logger,
|
|
106
130
|
cachePath: ".remote-state-cache.json",
|
|
@@ -112,7 +136,9 @@ async function main() {
|
|
|
112
136
|
const ssoStartUrl = args.values["sso-start-url"] ?? process.env.AWS_SSO_START_URL;
|
|
113
137
|
if (ssoStartUrl == null) {
|
|
114
138
|
printHelp(logger);
|
|
115
|
-
throw toUsageError(
|
|
139
|
+
throw toUsageError(
|
|
140
|
+
"--sso-start-url is required for the profile command (or set AWS_SSO_START_URL)."
|
|
141
|
+
);
|
|
116
142
|
}
|
|
117
143
|
await runProfileCommand({
|
|
118
144
|
logger,
|
|
@@ -136,7 +162,8 @@ async function main() {
|
|
|
136
162
|
yes: args.values.yes ?? false,
|
|
137
163
|
refresh: args.values.refresh ?? false,
|
|
138
164
|
allowDestructive: args.values["allow-destructive"] ?? false,
|
|
139
|
-
ignoreUnsupported: args.values["ignore-unsupported"] ?? false
|
|
165
|
+
ignoreUnsupported: args.values["ignore-unsupported"] ?? false,
|
|
166
|
+
update: args.values.update ?? false
|
|
140
167
|
},
|
|
141
168
|
logger,
|
|
142
169
|
overwriteConfirmation,
|
|
@@ -147,53 +174,73 @@ async function main() {
|
|
|
147
174
|
ssoAdminClient: new SSOAdminClient(clientConfig)
|
|
148
175
|
};
|
|
149
176
|
if (command === "bootstrap") {
|
|
150
|
-
|
|
177
|
+
await runRemoteBootstrap(remoteInput);
|
|
178
|
+
await printVersionBannerIfNeeded(logger);
|
|
179
|
+
return;
|
|
151
180
|
}
|
|
152
181
|
if (command === "scan") {
|
|
153
|
-
|
|
182
|
+
await runRemoteScan(remoteInput);
|
|
183
|
+
await printVersionBannerIfNeeded(logger);
|
|
184
|
+
return;
|
|
154
185
|
}
|
|
155
186
|
if (command === "init") {
|
|
156
|
-
|
|
187
|
+
await runRemoteInit(remoteInput);
|
|
188
|
+
await printVersionBannerIfNeeded(logger);
|
|
189
|
+
return;
|
|
157
190
|
}
|
|
158
191
|
if (command === "plan") {
|
|
159
|
-
|
|
192
|
+
await runRemotePlan(remoteInput);
|
|
193
|
+
await printVersionBannerIfNeeded(logger);
|
|
194
|
+
return;
|
|
160
195
|
}
|
|
161
196
|
if (command === "apply") {
|
|
162
|
-
|
|
197
|
+
await runRemoteApply(remoteInput);
|
|
198
|
+
await printVersionBannerIfNeeded(logger);
|
|
199
|
+
return;
|
|
163
200
|
}
|
|
164
201
|
if (command === "upgrade") {
|
|
165
|
-
|
|
202
|
+
await runRemoteUpgrade(remoteInput);
|
|
203
|
+
await printVersionBannerIfNeeded(logger);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (command === "drift") {
|
|
207
|
+
await runRemoteDrift(remoteInput);
|
|
208
|
+
await printVersionBannerIfNeeded(logger);
|
|
209
|
+
return;
|
|
166
210
|
}
|
|
167
|
-
|
|
168
|
-
process.exitCode = 1;
|
|
211
|
+
assertUnreachable(command, `Unhandled remote command: "${command}"`);
|
|
169
212
|
}
|
|
170
213
|
function printHelp(logger) {
|
|
214
|
+
const cmd = basename(process.argv[1], ".js");
|
|
171
215
|
logger.log("@beesolve/aws-accounts");
|
|
172
216
|
logger.log("");
|
|
173
217
|
logger.log("Usage:");
|
|
174
218
|
logger.log(
|
|
175
|
-
|
|
219
|
+
` ${cmd} bootstrap [--profile <name>] [--region <region>] [--yes]`
|
|
176
220
|
);
|
|
221
|
+
logger.log(` ${cmd} scan [--profile <name>] [--region <region>]`);
|
|
177
222
|
logger.log(
|
|
178
|
-
|
|
223
|
+
` ${cmd} init [--profile <name>] [--region <region>] [--yes]`
|
|
179
224
|
);
|
|
180
225
|
logger.log(
|
|
181
|
-
|
|
226
|
+
` ${cmd} init --update [--profile <name>] [--region <region>] [--yes]`
|
|
182
227
|
);
|
|
183
|
-
logger.log(
|
|
184
|
-
logger.log(
|
|
185
|
-
logger.log(
|
|
228
|
+
logger.log(` ${cmd} regenerate [--yes]`);
|
|
229
|
+
logger.log(` ${cmd} validate`);
|
|
230
|
+
logger.log(` ${cmd} graveyard`);
|
|
231
|
+
logger.log(` ${cmd} graveyard close`);
|
|
186
232
|
logger.log(
|
|
187
|
-
|
|
233
|
+
` ${cmd} profile --sso-start-url <url> [--sso-session <name>] (env: AWS_SSO_START_URL)`
|
|
188
234
|
);
|
|
189
235
|
logger.log(
|
|
190
|
-
|
|
236
|
+
` ${cmd} plan [--profile <name>] [--region <region>] [--refresh]`
|
|
191
237
|
);
|
|
192
238
|
logger.log(
|
|
193
|
-
|
|
239
|
+
` ${cmd} apply [--profile <name>] [--region <region>] [--yes] [--allow-destructive] [--ignore-unsupported]`
|
|
194
240
|
);
|
|
241
|
+
logger.log(` ${cmd} upgrade [--profile <name>] [--region <region>]`);
|
|
195
242
|
logger.log(
|
|
196
|
-
|
|
243
|
+
` ${cmd} drift [--profile <name>] [--region <region>] [--refresh]`
|
|
197
244
|
);
|
|
198
245
|
logger.log("");
|
|
199
246
|
logger.log("Environment fallback:");
|
|
@@ -227,6 +274,22 @@ function buildOverwriteConfirmation(props) {
|
|
|
227
274
|
}
|
|
228
275
|
};
|
|
229
276
|
}
|
|
277
|
+
async function printVersionBannerIfNeeded(logger) {
|
|
278
|
+
try {
|
|
279
|
+
const [context, currentVersion] = await Promise.all([
|
|
280
|
+
readAwsContextFromFile(contextPath),
|
|
281
|
+
readPackageVersion()
|
|
282
|
+
]);
|
|
283
|
+
const remoteVersion = context.deployment?.cliVersion;
|
|
284
|
+
if (remoteVersion != null && remoteVersion !== currentVersion) {
|
|
285
|
+
logger.log("");
|
|
286
|
+
logger.log(
|
|
287
|
+
`New version installed (local: ${currentVersion}, remote: ${remoteVersion}). Run upgrade then init --update to sync.`
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
} catch {
|
|
291
|
+
}
|
|
292
|
+
}
|
|
230
293
|
main().catch((error) => {
|
|
231
294
|
const classified = classifyCliError(error);
|
|
232
295
|
consoleLogger.error(`CLI ${classified.kind} error: ${classified.message}`);
|
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
import { readAwsContextFromFile } from "../awsConfig.js";
|
|
2
2
|
import { readStateCache } from "../remoteStateCache.js";
|
|
3
|
+
async function runGraveyardCloseCommand(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 graveyardOuId = context.organization.graveyardOuId;
|
|
14
|
+
const eligible = cache.state.organization.accounts.filter((a) => a.parentId === graveyardOuId && a.status === "ACTIVE").sort((a, b) => a.name.localeCompare(b.name));
|
|
15
|
+
if (eligible.length === 0) {
|
|
16
|
+
props.logger.log("No accounts eligible for closure in Graveyard.");
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
props.logger.log(`${eligible.length} account(s) eligible for closure:
|
|
20
|
+
`);
|
|
21
|
+
for (const account of eligible) {
|
|
22
|
+
props.logger.log(`# ${account.name} (${account.id})`);
|
|
23
|
+
props.logger.log(
|
|
24
|
+
`aws organizations close-account --account-id ${account.id}`
|
|
25
|
+
);
|
|
26
|
+
props.logger.log("");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
3
29
|
async function runGraveyardCommand(props) {
|
|
4
30
|
const [cache, context] = await Promise.all([
|
|
5
31
|
readStateCache(props.cachePath),
|
|
@@ -42,5 +68,6 @@ async function runGraveyardCommand(props) {
|
|
|
42
68
|
};
|
|
43
69
|
}
|
|
44
70
|
export {
|
|
71
|
+
runGraveyardCloseCommand,
|
|
45
72
|
runGraveyardCommand
|
|
46
73
|
};
|
package/dist/commands/remote.js
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
loadAwsConfigModelFromTsFile,
|
|
37
37
|
mapAwsConfigToState,
|
|
38
38
|
readAwsContextFromFile,
|
|
39
|
+
readPackageVersion,
|
|
39
40
|
regenerateTypesFromState,
|
|
40
41
|
writeAwsConfigFromState
|
|
41
42
|
} from "../awsConfig.js";
|
|
@@ -51,16 +52,18 @@ import {
|
|
|
51
52
|
import { applyReservedOuDeletionGuard } from "../reservedOuDeletion.js";
|
|
52
53
|
import { validateState } from "../state.js";
|
|
53
54
|
import { assertUnreachable, delay } from "../helpers.js";
|
|
55
|
+
import { toPreconditionError } from "../error.js";
|
|
54
56
|
import { sts, organizations, sso, identitystore, s3, logs, account, iam, lambda } from "@beesolve/iam-policy-ts";
|
|
55
57
|
const remoteCommandSchema = v.object({
|
|
56
|
-
subcommand: v.picklist(["bootstrap", "scan", "init", "plan", "apply", "upgrade"]),
|
|
58
|
+
subcommand: v.picklist(["bootstrap", "scan", "init", "plan", "apply", "upgrade", "drift"]),
|
|
57
59
|
profile: v.optional(v.string()),
|
|
58
60
|
region: v.optional(v.string()),
|
|
59
61
|
flags: v.object({
|
|
60
62
|
yes: v.boolean(),
|
|
61
63
|
refresh: v.boolean(),
|
|
62
64
|
allowDestructive: v.boolean(),
|
|
63
|
-
ignoreUnsupported: v.boolean()
|
|
65
|
+
ignoreUnsupported: v.boolean(),
|
|
66
|
+
update: v.boolean()
|
|
64
67
|
})
|
|
65
68
|
});
|
|
66
69
|
const contextFilePath = "aws.context.json";
|
|
@@ -121,12 +124,14 @@ async function runRemoteBootstrap(input) {
|
|
|
121
124
|
context = await readAwsContextFromFile(contextFilePath);
|
|
122
125
|
} catch {
|
|
123
126
|
}
|
|
127
|
+
const cliVersionForBootstrap = await readPackageVersion();
|
|
124
128
|
const deployment = {
|
|
125
129
|
profile: input.profile ?? "",
|
|
126
130
|
region: resolvedRegion,
|
|
127
131
|
lambdaArn,
|
|
128
132
|
stateBucketName: bucketName,
|
|
129
|
-
stateCacheTtlSeconds: 300
|
|
133
|
+
stateCacheTtlSeconds: 300,
|
|
134
|
+
cliVersion: cliVersionForBootstrap
|
|
130
135
|
};
|
|
131
136
|
const updatedContext = context != null ? { ...context, deployment } : {
|
|
132
137
|
version: "1",
|
|
@@ -172,22 +177,7 @@ async function runRemoteBootstrap(input) {
|
|
|
172
177
|
input.logger.log(` Lambda ARN: ${lambdaArn}`);
|
|
173
178
|
input.logger.log(` State bucket: ${bucketName}`);
|
|
174
179
|
}
|
|
175
|
-
async function
|
|
176
|
-
const trustPolicy = JSON.stringify({
|
|
177
|
-
Version: "2012-10-17",
|
|
178
|
-
Statement: [
|
|
179
|
-
{
|
|
180
|
-
Effect: "Allow",
|
|
181
|
-
Principal: { Service: "lambda.amazonaws.com" },
|
|
182
|
-
Action: sts("AssumeRole")
|
|
183
|
-
}
|
|
184
|
-
]
|
|
185
|
-
});
|
|
186
|
-
const { roleArn } = await getOrCreateIamRole({
|
|
187
|
-
iamClient: props.iamClient,
|
|
188
|
-
trustPolicy,
|
|
189
|
-
logger: props.logger
|
|
190
|
-
});
|
|
180
|
+
async function applyLambdaRolePolicy(props) {
|
|
191
181
|
const inlinePolicy = JSON.stringify({
|
|
192
182
|
Version: "2012-10-17",
|
|
193
183
|
Statement: [
|
|
@@ -220,7 +210,12 @@ async function ensureIamRole(props) {
|
|
|
220
210
|
},
|
|
221
211
|
{
|
|
222
212
|
Effect: "Allow",
|
|
223
|
-
Action: [
|
|
213
|
+
Action: [
|
|
214
|
+
account("PutAccountName"),
|
|
215
|
+
account("GetAlternateContact"),
|
|
216
|
+
account("PutAlternateContact"),
|
|
217
|
+
account("DeleteAlternateContact")
|
|
218
|
+
],
|
|
224
219
|
Resource: "*"
|
|
225
220
|
}
|
|
226
221
|
]
|
|
@@ -232,6 +227,27 @@ async function ensureIamRole(props) {
|
|
|
232
227
|
PolicyDocument: inlinePolicy
|
|
233
228
|
})
|
|
234
229
|
);
|
|
230
|
+
}
|
|
231
|
+
async function ensureIamRole(props) {
|
|
232
|
+
const trustPolicy = JSON.stringify({
|
|
233
|
+
Version: "2012-10-17",
|
|
234
|
+
Statement: [
|
|
235
|
+
{
|
|
236
|
+
Effect: "Allow",
|
|
237
|
+
Principal: { Service: "lambda.amazonaws.com" },
|
|
238
|
+
Action: sts("AssumeRole")
|
|
239
|
+
}
|
|
240
|
+
]
|
|
241
|
+
});
|
|
242
|
+
const { roleArn } = await getOrCreateIamRole({
|
|
243
|
+
iamClient: props.iamClient,
|
|
244
|
+
trustPolicy,
|
|
245
|
+
logger: props.logger
|
|
246
|
+
});
|
|
247
|
+
await applyLambdaRolePolicy({
|
|
248
|
+
iamClient: props.iamClient,
|
|
249
|
+
bucketName: props.bucketName
|
|
250
|
+
});
|
|
235
251
|
return { roleArn };
|
|
236
252
|
}
|
|
237
253
|
async function getOrCreateIamRole(props) {
|
|
@@ -395,11 +411,27 @@ async function runRemoteScan(input) {
|
|
|
395
411
|
input.logger.log(` Groups: ${response.summary.groups}`);
|
|
396
412
|
input.logger.log(` Permission Sets: ${response.summary.permissionSets}`);
|
|
397
413
|
input.logger.log(` Account Assignments: ${response.summary.accountAssignments}`);
|
|
414
|
+
input.logger.log(` Policies: ${response.summary.policies}`);
|
|
415
|
+
input.logger.log(` Policy Attachments: ${response.summary.policyAttachments}`);
|
|
398
416
|
await writeStateCache(cachePath, response.state);
|
|
399
417
|
input.logger.log("State cache updated.");
|
|
400
418
|
}
|
|
401
419
|
async function runRemoteInit(input) {
|
|
402
|
-
const
|
|
420
|
+
const isUpdate = input.flags.update;
|
|
421
|
+
let existingConfig;
|
|
422
|
+
if (isUpdate) {
|
|
423
|
+
try {
|
|
424
|
+
existingConfig = await loadAwsConfigModelFromTsFile({
|
|
425
|
+
configPath: configFilePath,
|
|
426
|
+
typesPath: typesFilePath
|
|
427
|
+
});
|
|
428
|
+
} catch {
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
const [deployment, cliVersion] = await Promise.all([
|
|
432
|
+
readDeploymentFromContext(),
|
|
433
|
+
readPackageVersion()
|
|
434
|
+
]);
|
|
403
435
|
input.logger.log("Invoking remote scan...");
|
|
404
436
|
const result = await invokeLambda({
|
|
405
437
|
lambdaClient: input.lambdaClient,
|
|
@@ -420,14 +452,17 @@ async function runRemoteInit(input) {
|
|
|
420
452
|
input.logger.log(` Groups: ${response.summary.groups}`);
|
|
421
453
|
input.logger.log(` Permission Sets: ${response.summary.permissionSets}`);
|
|
422
454
|
input.logger.log(` Account Assignments: ${response.summary.accountAssignments}`);
|
|
455
|
+
input.logger.log(` Policies: ${response.summary.policies}`);
|
|
456
|
+
input.logger.log(` Policy Attachments: ${response.summary.policyAttachments}`);
|
|
423
457
|
await writeStateCache(cachePath, response.state);
|
|
424
458
|
input.logger.log("State cache updated.");
|
|
425
459
|
const context = await readAwsContextFromFile(contextFilePath);
|
|
426
460
|
const graveyardOu = response.state.organization.organizationalUnits.find(
|
|
427
461
|
(ou) => ou.name === "Graveyard"
|
|
428
462
|
);
|
|
429
|
-
const
|
|
430
|
-
|
|
463
|
+
const ordered = {
|
|
464
|
+
version: context.version,
|
|
465
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
431
466
|
organization: {
|
|
432
467
|
managementAccountId: context.organization.managementAccountId,
|
|
433
468
|
rootId: response.state.organization.rootId,
|
|
@@ -436,14 +471,8 @@ async function runRemoteInit(input) {
|
|
|
436
471
|
identityCenter: {
|
|
437
472
|
instanceArn: response.state.identityCenter.instanceArn,
|
|
438
473
|
identityStoreId: response.state.identityCenter.identityStoreId
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
const ordered = {
|
|
442
|
-
version: updatedContext.version,
|
|
443
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
444
|
-
organization: updatedContext.organization,
|
|
445
|
-
identityCenter: updatedContext.identityCenter,
|
|
446
|
-
deployment: updatedContext.deployment
|
|
474
|
+
},
|
|
475
|
+
deployment: { ...deployment, cliVersion }
|
|
447
476
|
};
|
|
448
477
|
await writeFile(contextFilePath, `${JSON.stringify(ordered, null, 2)}
|
|
449
478
|
`, "utf8");
|
|
@@ -453,12 +482,13 @@ async function runRemoteInit(input) {
|
|
|
453
482
|
configPath: configFilePath,
|
|
454
483
|
typesPath: typesFilePath,
|
|
455
484
|
logger: input.logger,
|
|
456
|
-
overwriteConfirmation: input.overwriteConfirmation
|
|
485
|
+
overwriteConfirmation: input.overwriteConfirmation,
|
|
486
|
+
existingConfig
|
|
457
487
|
});
|
|
458
488
|
const writtenFiles = configWriteResult.files.filter((f) => f.status === "written");
|
|
459
489
|
if (writtenFiles.length > 0) {
|
|
460
490
|
input.logger.log("");
|
|
461
|
-
input.logger.log("Init complete.");
|
|
491
|
+
input.logger.log(isUpdate ? "Init --update complete." : "Init complete.");
|
|
462
492
|
for (const file of writtenFiles) {
|
|
463
493
|
input.logger.log(` ${file.path}: ${file.status}`);
|
|
464
494
|
}
|
|
@@ -477,6 +507,7 @@ async function runRemotePlan(input) {
|
|
|
477
507
|
typesPath: typesFilePath
|
|
478
508
|
})
|
|
479
509
|
]);
|
|
510
|
+
warnIfRemotePoliciesNotInConfig({ currentState, config, logger: input.logger });
|
|
480
511
|
const desiredState = mapAwsConfigToState({
|
|
481
512
|
config,
|
|
482
513
|
currentState,
|
|
@@ -504,6 +535,7 @@ async function runRemoteApply(input) {
|
|
|
504
535
|
typesPath: typesFilePath
|
|
505
536
|
})
|
|
506
537
|
]);
|
|
538
|
+
warnIfRemotePoliciesNotInConfig({ currentState, config, logger: input.logger });
|
|
507
539
|
const desiredState = mapAwsConfigToState({
|
|
508
540
|
config,
|
|
509
541
|
currentState,
|
|
@@ -591,8 +623,11 @@ async function runRemoteApply(input) {
|
|
|
591
623
|
});
|
|
592
624
|
}
|
|
593
625
|
async function runRemoteUpgrade(input) {
|
|
594
|
-
const deployment = await
|
|
595
|
-
|
|
626
|
+
const [deployment, cliVersion, lambdaZip] = await Promise.all([
|
|
627
|
+
readDeploymentFromContext(),
|
|
628
|
+
readPackageVersion(),
|
|
629
|
+
readLambdaZip()
|
|
630
|
+
]);
|
|
596
631
|
input.logger.log(`Updating Lambda function code: ${deployment.lambdaArn}`);
|
|
597
632
|
await waitForLambdaReady(input.lambdaClient, deployment.lambdaArn);
|
|
598
633
|
const updateResult = await input.lambdaClient.send(
|
|
@@ -602,12 +637,102 @@ async function runRemoteUpgrade(input) {
|
|
|
602
637
|
})
|
|
603
638
|
);
|
|
604
639
|
const lastModified = updateResult.LastModified ?? "unknown";
|
|
605
|
-
input.logger.log(`
|
|
640
|
+
input.logger.log(`Lambda updated. Last modified: ${lastModified}`);
|
|
641
|
+
input.logger.log("Updating IAM role policy...");
|
|
642
|
+
await applyLambdaRolePolicy({
|
|
643
|
+
iamClient: input.iamClient,
|
|
644
|
+
bucketName: deployment.stateBucketName
|
|
645
|
+
});
|
|
646
|
+
input.logger.log("IAM role policy updated.");
|
|
647
|
+
const context = await readAwsContextFromFile(contextFilePath);
|
|
648
|
+
const ordered = {
|
|
649
|
+
version: context.version,
|
|
650
|
+
generatedAt: context.generatedAt,
|
|
651
|
+
organization: context.organization,
|
|
652
|
+
identityCenter: context.identityCenter,
|
|
653
|
+
deployment: { ...deployment, cliVersion }
|
|
654
|
+
};
|
|
655
|
+
await writeFile(contextFilePath, `${JSON.stringify(ordered, null, 2)}
|
|
656
|
+
`, "utf8");
|
|
657
|
+
input.logger.log("");
|
|
658
|
+
input.logger.log("Run init --update to sync your config with new remote features before using plan/apply.");
|
|
659
|
+
}
|
|
660
|
+
async function runRemoteDrift(input) {
|
|
661
|
+
const deployment = await readDeploymentFromContext();
|
|
662
|
+
const baseline = await fetchCurrentState({
|
|
663
|
+
input,
|
|
664
|
+
deployment
|
|
665
|
+
});
|
|
666
|
+
const clientConfig = buildAwsClientConfig({
|
|
667
|
+
profile: input.profile ?? (deployment.profile || void 0),
|
|
668
|
+
region: input.region ?? (deployment.region || void 0)
|
|
669
|
+
});
|
|
670
|
+
const lambdaClient = new LambdaClient(clientConfig);
|
|
671
|
+
input.logger.log("Scanning live AWS state...");
|
|
672
|
+
const result = await invokeLambda({
|
|
673
|
+
lambdaClient,
|
|
674
|
+
lambdaArn: deployment.lambdaArn,
|
|
675
|
+
payload: { action: "scan" }
|
|
676
|
+
});
|
|
677
|
+
if (!result.ok) {
|
|
678
|
+
throw new Error(formatLambdaError(result.error));
|
|
679
|
+
}
|
|
680
|
+
const response = result.response;
|
|
681
|
+
if (!("action" in response) || response.action !== "scan") {
|
|
682
|
+
throw new Error("Unexpected response from Lambda scan action.");
|
|
683
|
+
}
|
|
684
|
+
const liveState = response.state;
|
|
685
|
+
await writeStateCache(cachePath, liveState);
|
|
686
|
+
const plan = diffStates({
|
|
687
|
+
current: baseline,
|
|
688
|
+
next: liveState
|
|
689
|
+
});
|
|
690
|
+
displayDrift({ plan, logger: input.logger });
|
|
691
|
+
}
|
|
692
|
+
function displayDrift(props) {
|
|
693
|
+
const driftOperations = props.plan.operations.filter(
|
|
694
|
+
(operation) => operation.kind !== "provisionIdcPermissionSet"
|
|
695
|
+
);
|
|
696
|
+
if (driftOperations.length === 0 && props.plan.unsupported.length === 0) {
|
|
697
|
+
props.logger.log("No drift.");
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
props.logger.log(`Drift: ${driftOperations.length} change(s) detected since last scan`);
|
|
701
|
+
for (const operation of driftOperations) {
|
|
702
|
+
props.logger.log(formatOperationLine(operation));
|
|
703
|
+
}
|
|
704
|
+
if (props.plan.unsupported.length > 0) {
|
|
705
|
+
props.logger.log("Unsupported diffs:");
|
|
706
|
+
for (const diff of props.plan.unsupported) {
|
|
707
|
+
props.logger.log(` - ${diff.description} [${diff.category}]`);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
function warnIfRemotePoliciesNotInConfig(props) {
|
|
712
|
+
const remotePolicies = props.currentState.organization.policies ?? [];
|
|
713
|
+
const hasRemotePolicies = remotePolicies.length > 0;
|
|
714
|
+
const hasLocalPolicies = props.config.policies.serviceControlPolicies.length > 0 || props.config.policies.resourceControlPolicies.length > 0;
|
|
715
|
+
if (hasRemotePolicies && !hasLocalPolicies) {
|
|
716
|
+
props.logger.log("");
|
|
717
|
+
props.logger.log("Warning: remote state contains SCPs/RCPs not present in your config. Proceeding could delete them.");
|
|
718
|
+
props.logger.log("Run init --update to sync first.");
|
|
719
|
+
props.logger.log("");
|
|
720
|
+
}
|
|
606
721
|
}
|
|
607
722
|
async function readDeploymentFromContext() {
|
|
608
|
-
|
|
723
|
+
let context;
|
|
724
|
+
try {
|
|
725
|
+
context = await readAwsContextFromFile(contextFilePath);
|
|
726
|
+
} catch (err) {
|
|
727
|
+
if (err.code === "ENOENT") {
|
|
728
|
+
throw toPreconditionError(
|
|
729
|
+
"aws.context.json not found. Run `aws-accounts bootstrap` first."
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
throw err;
|
|
733
|
+
}
|
|
609
734
|
if (context.deployment == null) {
|
|
610
|
-
throw
|
|
735
|
+
throw toPreconditionError(
|
|
611
736
|
"No deployment found in aws.context.json. Run `aws-accounts bootstrap` first."
|
|
612
737
|
);
|
|
613
738
|
}
|
|
@@ -674,7 +799,7 @@ function displayPlan(props) {
|
|
|
674
799
|
}
|
|
675
800
|
}
|
|
676
801
|
function isDestructiveOperation(operation) {
|
|
677
|
-
return operation.kind === "deleteOu" || operation.kind === "removeAccount" || operation.kind === "deleteIdcUser" || operation.kind === "deleteIdcGroup" || operation.kind === "deleteIdcPermissionSet";
|
|
802
|
+
return operation.kind === "deleteOu" || operation.kind === "removeAccount" || operation.kind === "deleteIdcUser" || operation.kind === "deleteIdcGroup" || operation.kind === "deleteIdcPermissionSet" || operation.kind === "deleteIdcPermissionSetPermissionsBoundary" || operation.kind === "detachOrgPolicy" || operation.kind === "deleteOrgPolicy" || operation.kind === "deregisterDelegatedAdministrator";
|
|
678
803
|
}
|
|
679
804
|
function formatOperationLine(operation) {
|
|
680
805
|
if (operation.kind === "moveAccount") {
|
|
@@ -752,6 +877,14 @@ function formatOperationLine(operation) {
|
|
|
752
877
|
if (operation.kind === "provisionIdcPermissionSet") {
|
|
753
878
|
return ` provision IdC permission set "${operation.permissionSetName}" to all provisioned accounts`;
|
|
754
879
|
}
|
|
880
|
+
if (operation.kind === "putIdcPermissionSetPermissionsBoundary") {
|
|
881
|
+
const b = operation.permissionsBoundary;
|
|
882
|
+
const label = "managedPolicyArn" in b ? b.managedPolicyArn : `${b.customerManagedPolicyPath}${b.customerManagedPolicyName}`;
|
|
883
|
+
return ` put permissions boundary "${label}" on IdC permission set "${operation.permissionSetName}"`;
|
|
884
|
+
}
|
|
885
|
+
if (operation.kind === "deleteIdcPermissionSetPermissionsBoundary") {
|
|
886
|
+
return ` [destructive] delete permissions boundary from IdC permission set "${operation.permissionSetName}"`;
|
|
887
|
+
}
|
|
755
888
|
if (operation.kind === "removeIdcGroupMembership") {
|
|
756
889
|
return ` remove user "${operation.userName}" from IdC group "${operation.groupDisplayName}"`;
|
|
757
890
|
}
|
|
@@ -765,6 +898,39 @@ function formatOperationLine(operation) {
|
|
|
765
898
|
const duration = operation.sessionDuration ?? "default";
|
|
766
899
|
return ` update IdC permission set session duration "${operation.permissionSetName}" -> ${duration}`;
|
|
767
900
|
}
|
|
901
|
+
if (operation.kind === "createOrgPolicy") {
|
|
902
|
+
return ` create org policy "${operation.policyName}" (${operation.policyType})`;
|
|
903
|
+
}
|
|
904
|
+
if (operation.kind === "updateOrgPolicyContent") {
|
|
905
|
+
return ` update org policy content "${operation.policyName}"`;
|
|
906
|
+
}
|
|
907
|
+
if (operation.kind === "updateOrgPolicyDescription") {
|
|
908
|
+
return ` update org policy description "${operation.policyName}"`;
|
|
909
|
+
}
|
|
910
|
+
if (operation.kind === "attachOrgPolicy") {
|
|
911
|
+
return ` attach org policy "${operation.policyName}" to "${operation.targetName}"`;
|
|
912
|
+
}
|
|
913
|
+
if (operation.kind === "detachOrgPolicy") {
|
|
914
|
+
return ` [destructive] detach org policy "${operation.policyName}" from "${operation.targetName}"`;
|
|
915
|
+
}
|
|
916
|
+
if (operation.kind === "deleteOrgPolicy") {
|
|
917
|
+
return ` [destructive] delete org policy "${operation.policyName}"`;
|
|
918
|
+
}
|
|
919
|
+
if (operation.kind === "putAlternateContact") {
|
|
920
|
+
return ` set ${operation.contactType} alternate contact for "${operation.accountName}" (${operation.accountId})`;
|
|
921
|
+
}
|
|
922
|
+
if (operation.kind === "deleteAlternateContact") {
|
|
923
|
+
return ` [destructive] delete ${operation.contactType} alternate contact for "${operation.accountName}" (${operation.accountId})`;
|
|
924
|
+
}
|
|
925
|
+
if (operation.kind === "setIdcAccessControlAttributes") {
|
|
926
|
+
return ` set IdC access control attributes (${operation.attributes.length} attribute(s))`;
|
|
927
|
+
}
|
|
928
|
+
if (operation.kind === "registerDelegatedAdministrator") {
|
|
929
|
+
return ` register delegated administrator "${operation.accountName}" (${operation.accountId}) for ${operation.servicePrincipal}`;
|
|
930
|
+
}
|
|
931
|
+
if (operation.kind === "deregisterDelegatedAdministrator") {
|
|
932
|
+
return ` [destructive] deregister delegated administrator "${operation.accountName}" (${operation.accountId}) for ${operation.servicePrincipal}`;
|
|
933
|
+
}
|
|
768
934
|
assertUnreachable(operation, "Unsupported operation kind in formatOperationLine.");
|
|
769
935
|
}
|
|
770
936
|
function formatPrincipalLabel(principalType, principalName) {
|
|
@@ -976,6 +1142,7 @@ async function waitForLambdaReady(lambdaClient, functionName) {
|
|
|
976
1142
|
export {
|
|
977
1143
|
runRemoteApply,
|
|
978
1144
|
runRemoteBootstrap,
|
|
1145
|
+
runRemoteDrift,
|
|
979
1146
|
runRemoteInit,
|
|
980
1147
|
runRemotePlan,
|
|
981
1148
|
runRemoteScan,
|