alchemy 0.0.7 → 0.1.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/package.json +23 -22
- package/src/alchemize.ts +151 -0
- package/src/apply.ts +117 -0
- package/src/components/agent/agent.ts +45 -0
- package/src/components/agent/anthropic.ts +27 -0
- package/src/components/agent/index.ts +4 -0
- package/src/components/agent/model.ts +22 -0
- package/src/components/agent/openai.ts +44 -0
- package/src/components/agent/package.ts +126 -0
- package/src/components/agent/requirements.ts +111 -0
- package/src/components/agent/scrape.ts +47 -0
- package/src/components/agent/tsconfig.ts +150 -0
- package/src/components/agent/typescript.ts +177 -0
- package/src/components/aws/bucket.ts +168 -0
- package/src/components/aws/function.ts +302 -0
- package/src/components/aws/index.ts +6 -0
- package/src/components/aws/policy.ts +245 -0
- package/src/components/aws/queue.ts +222 -0
- package/src/components/aws/role.ts +245 -0
- package/src/components/aws/table.ts +172 -0
- package/src/components/esbuild.ts +128 -0
- package/src/components/fs.ts +45 -0
- package/src/destroy.ts +63 -0
- package/src/error.ts +16 -0
- package/src/global.ts +72 -0
- package/src/index.ts +6 -0
- package/src/input.ts +16 -0
- package/src/main.ts +34 -0
- package/src/output.ts +49 -0
- package/src/resource.ts +313 -0
- package/src/state.ts +100 -0
- package/test/aws/function.test.ts +144 -0
- package/test/aws/queue.test.ts +85 -0
- package/test/aws/role.test.ts +153 -0
- package/test/aws/table.test.ts +73 -0
- package/test/esbuild.test.ts +41 -0
- package/test/handler.ts +9 -0
- package/tsconfig.json +7 -0
- package/Column.js +0 -1
- package/Create.js +0 -1
- package/Delete.js +0 -1
- package/Drop.js +0 -1
- package/Insert.js +0 -1
- package/Makefile +0 -8
- package/README.md +0 -0
- package/Select.js +0 -1
- package/Table.js +0 -1
- package/Transaction.js +0 -1
- package/Types.js +0 -1
- package/Update.js +0 -1
- package/alchemy.js +0 -57
- package/alchemy.sublime-project +0 -9
- package/alchemy.sublime-workspace +0 -2304
- package/config/app.js +0 -8
- package/config/db.js +0 -20
- package/copy.js +0 -1
- package/defaultEngine.js +0 -8
- package/engine.js +0 -282
- package/getInstantiatorFunction.js +0 -28
- package/index.js +0 -52
- package/jstest.js +0 -3
- package/loggerFactory.js +0 -27
- package/logs/alchemy.log +0 -46
- package/logs/sql.log +0 -41
- package/sql/Column.js +0 -158
- package/sql/Columnable.js +0 -21
- package/sql/Create.js +0 -15
- package/sql/Delete.js +0 -34
- package/sql/Drop.js +0 -36
- package/sql/Insert.js +0 -96
- package/sql/SchemaElement.js +0 -11
- package/sql/Select.js +0 -192
- package/sql/SqlBase.js +0 -134
- package/sql/SqlStatement.js +0 -80
- package/sql/Table.js +0 -96
- package/sql/Transaction.js +0 -45
- package/sql/Update.js +0 -105
- package/sql/defineElement.js +0 -26
- package/sql/defineStatement.js +0 -26
- package/sql/types/BaseType.js +0 -43
- package/sql/types/BigSerialType.js +0 -5
- package/sql/types/IntegerType.js +0 -5
- package/sql/types/JsonType.js +0 -5
- package/sql/types/SerialType.js +0 -5
- package/sql/types/StringType.js +0 -31
- package/sql/types/TimestampType.js +0 -5
- package/sql/types/UUIDType.js +0 -5
- package/sql/types/defineType.js +0 -25
- package/sql/types/getType.js +0 -12
- package/sql/types/index.js +0 -11
- package/sql/where.js +0 -31
- package/sql/whereToString.js +0 -29
- package/test/ColumnTests.js +0 -80
- package/test/CreateTable.js +0 -54
- package/test/DeleteTests.js +0 -28
- package/test/DropTests.js +0 -23
- package/test/GetTypeTests.js +0 -56
- package/test/InsertTests.js +0 -86
- package/test/SelectExistsTests.js +0 -26
- package/test/SelectTests.js +0 -130
- package/test/TableTests.js +0 -46
- package/test.js +0 -8
- package/test.ls +0 -4
- package/validate.js +0 -26
- package/x.js +0 -5
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Architecture,
|
|
3
|
+
CreateFunctionCommand,
|
|
4
|
+
DeleteFunctionCommand,
|
|
5
|
+
GetFunctionCommand,
|
|
6
|
+
GetFunctionConfigurationCommand,
|
|
7
|
+
LambdaClient,
|
|
8
|
+
ResourceNotFoundException,
|
|
9
|
+
Runtime,
|
|
10
|
+
UpdateFunctionCodeCommand,
|
|
11
|
+
UpdateFunctionConfigurationCommand,
|
|
12
|
+
} from "@aws-sdk/client-lambda";
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { ignore } from "../../error";
|
|
16
|
+
import { Resource, type Context } from "../../resource";
|
|
17
|
+
|
|
18
|
+
async function resolveRegion(client: LambdaClient): Promise<string> {
|
|
19
|
+
const region = client.config.region;
|
|
20
|
+
if (typeof region === "string") return region;
|
|
21
|
+
if (typeof region === "function") return region();
|
|
22
|
+
throw new Error("Could not resolve AWS region");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface FunctionProps {
|
|
26
|
+
functionName: string;
|
|
27
|
+
zipPath: string;
|
|
28
|
+
roleArn: string;
|
|
29
|
+
handler?: string;
|
|
30
|
+
runtime?: Runtime;
|
|
31
|
+
architecture?: Architecture;
|
|
32
|
+
description?: string;
|
|
33
|
+
timeout?: number;
|
|
34
|
+
memorySize?: number;
|
|
35
|
+
environment?: Record<string, string>;
|
|
36
|
+
tags?: Record<string, string>;
|
|
37
|
+
url?: {
|
|
38
|
+
authType?: "AWS_IAM" | "NONE";
|
|
39
|
+
cors?: {
|
|
40
|
+
allowCredentials?: boolean;
|
|
41
|
+
allowHeaders?: string[];
|
|
42
|
+
allowMethods?: string[];
|
|
43
|
+
allowOrigins?: string[];
|
|
44
|
+
exposeHeaders?: string[];
|
|
45
|
+
maxAge?: number;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface FunctionOutput extends FunctionProps {
|
|
51
|
+
id: string; // Same as functionName
|
|
52
|
+
arn: string;
|
|
53
|
+
lastModified: string;
|
|
54
|
+
version: string;
|
|
55
|
+
qualifiedArn: string; // ARN with version
|
|
56
|
+
invokeArn: string; // ARN for API Gateway
|
|
57
|
+
sourceCodeHash: string;
|
|
58
|
+
sourceCodeSize: number;
|
|
59
|
+
ephemeralStorageSize?: number;
|
|
60
|
+
architectures: string[];
|
|
61
|
+
masterArn?: string; // Only for Lambda@Edge
|
|
62
|
+
revisionId: string;
|
|
63
|
+
state?: string;
|
|
64
|
+
stateReason?: string;
|
|
65
|
+
stateReasonCode?: string;
|
|
66
|
+
lastUpdateStatus?: string;
|
|
67
|
+
lastUpdateStatusReason?: string;
|
|
68
|
+
lastUpdateStatusReasonCode?: string;
|
|
69
|
+
packageType: string;
|
|
70
|
+
signingProfileVersionArn?: string;
|
|
71
|
+
signingJobArn?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export class Function extends Resource(
|
|
75
|
+
"lambda::Function",
|
|
76
|
+
async (ctx: Context<FunctionOutput>, props: FunctionProps) => {
|
|
77
|
+
const client = new LambdaClient({});
|
|
78
|
+
const region = await resolveRegion(client);
|
|
79
|
+
|
|
80
|
+
const code = await zipCode(props.zipPath);
|
|
81
|
+
|
|
82
|
+
if (ctx.event === "delete") {
|
|
83
|
+
await ignore(ResourceNotFoundException.name, () =>
|
|
84
|
+
client.send(
|
|
85
|
+
new DeleteFunctionCommand({
|
|
86
|
+
FunctionName: props.functionName,
|
|
87
|
+
}),
|
|
88
|
+
),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// Return a minimal FunctionOutput for deleted state
|
|
92
|
+
return {
|
|
93
|
+
...props,
|
|
94
|
+
id: props.functionName,
|
|
95
|
+
arn: `arn:aws:lambda:${region}:${process.env.AWS_ACCOUNT_ID}:function:${props.functionName}`,
|
|
96
|
+
lastModified: new Date().toISOString(),
|
|
97
|
+
version: "$LATEST",
|
|
98
|
+
qualifiedArn: `arn:aws:lambda:${region}:${process.env.AWS_ACCOUNT_ID}:function:${props.functionName}:$LATEST`,
|
|
99
|
+
invokeArn: `arn:aws:apigateway:${region}:lambda:path/2015-03-31/functions/arn:aws:lambda:${region}:${process.env.AWS_ACCOUNT_ID}:function:${props.functionName}/invocations`,
|
|
100
|
+
sourceCodeHash: "",
|
|
101
|
+
sourceCodeSize: 0,
|
|
102
|
+
architectures: props.architecture
|
|
103
|
+
? [props.architecture]
|
|
104
|
+
: [Architecture.x86_64],
|
|
105
|
+
revisionId: "",
|
|
106
|
+
state: "Inactive",
|
|
107
|
+
packageType: "Zip",
|
|
108
|
+
};
|
|
109
|
+
} else {
|
|
110
|
+
try {
|
|
111
|
+
// Check if function exists
|
|
112
|
+
const func = await client.send(
|
|
113
|
+
new GetFunctionCommand({
|
|
114
|
+
FunctionName: props.functionName,
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
if (ctx.event === "update") {
|
|
119
|
+
// Wait for function to stabilize
|
|
120
|
+
await waitForFunctionStabilization(client, props.functionName);
|
|
121
|
+
|
|
122
|
+
// Update function code
|
|
123
|
+
await client.send(
|
|
124
|
+
new UpdateFunctionCodeCommand({
|
|
125
|
+
FunctionName: props.functionName,
|
|
126
|
+
ZipFile: code,
|
|
127
|
+
}),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// Wait for code update to stabilize
|
|
131
|
+
await waitForFunctionStabilization(client, props.functionName);
|
|
132
|
+
|
|
133
|
+
// Update function configuration
|
|
134
|
+
await client.send(
|
|
135
|
+
new UpdateFunctionConfigurationCommand({
|
|
136
|
+
FunctionName: props.functionName,
|
|
137
|
+
Handler: props.handler,
|
|
138
|
+
Runtime: props.runtime,
|
|
139
|
+
Role: props.roleArn,
|
|
140
|
+
Description: props.description,
|
|
141
|
+
Timeout: props.timeout,
|
|
142
|
+
MemorySize: props.memorySize,
|
|
143
|
+
Environment: props.environment
|
|
144
|
+
? { Variables: props.environment }
|
|
145
|
+
: undefined,
|
|
146
|
+
}),
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// Wait for configuration update to stabilize
|
|
150
|
+
await waitForFunctionStabilization(client, props.functionName);
|
|
151
|
+
}
|
|
152
|
+
} catch (error: any) {
|
|
153
|
+
if (error.name === "ResourceNotFoundException") {
|
|
154
|
+
// Create function if it doesn't exist
|
|
155
|
+
const startTime = Date.now();
|
|
156
|
+
let delay = 100; // Start with 100ms delay
|
|
157
|
+
|
|
158
|
+
while (true) {
|
|
159
|
+
try {
|
|
160
|
+
await client.send(
|
|
161
|
+
new CreateFunctionCommand({
|
|
162
|
+
FunctionName: props.functionName,
|
|
163
|
+
Code: { ZipFile: code },
|
|
164
|
+
Handler: props.handler || "index.handler",
|
|
165
|
+
Runtime: props.runtime || Runtime.nodejs20x,
|
|
166
|
+
Role: props.roleArn,
|
|
167
|
+
Description: props.description,
|
|
168
|
+
Timeout: props.timeout || 3,
|
|
169
|
+
MemorySize: props.memorySize || 128,
|
|
170
|
+
Environment: props.environment
|
|
171
|
+
? { Variables: props.environment }
|
|
172
|
+
: undefined,
|
|
173
|
+
Architectures: props.architecture
|
|
174
|
+
? [props.architecture]
|
|
175
|
+
: [Architecture.x86_64],
|
|
176
|
+
Tags: props.tags,
|
|
177
|
+
}),
|
|
178
|
+
);
|
|
179
|
+
break; // Success - exit retry loop
|
|
180
|
+
} catch (createError: any) {
|
|
181
|
+
if (
|
|
182
|
+
createError.name !== "InvalidParameterValueException" ||
|
|
183
|
+
!createError.message?.includes("cannot be assumed by Lambda")
|
|
184
|
+
) {
|
|
185
|
+
throw createError; // Different error - rethrow
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (Date.now() - startTime > 10000) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
"Timeout waiting for IAM role to be assumable by Lambda after 10s",
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
195
|
+
delay = Math.min(delay * 2, 1000); // Exponential backoff capped at 1s
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Wait for function to be active
|
|
200
|
+
let isCreating = true;
|
|
201
|
+
while (isCreating) {
|
|
202
|
+
const config = await client.send(
|
|
203
|
+
new GetFunctionConfigurationCommand({
|
|
204
|
+
FunctionName: props.functionName,
|
|
205
|
+
}),
|
|
206
|
+
);
|
|
207
|
+
isCreating = config.State === "Pending";
|
|
208
|
+
if (isCreating) {
|
|
209
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Get complete function details
|
|
218
|
+
const [func, config] = await Promise.all([
|
|
219
|
+
client.send(
|
|
220
|
+
new GetFunctionCommand({
|
|
221
|
+
FunctionName: props.functionName,
|
|
222
|
+
}),
|
|
223
|
+
),
|
|
224
|
+
client.send(
|
|
225
|
+
new GetFunctionConfigurationCommand({
|
|
226
|
+
FunctionName: props.functionName,
|
|
227
|
+
}),
|
|
228
|
+
),
|
|
229
|
+
]);
|
|
230
|
+
|
|
231
|
+
const output: FunctionOutput = {
|
|
232
|
+
...props,
|
|
233
|
+
id: props.functionName,
|
|
234
|
+
arn: config.FunctionArn!,
|
|
235
|
+
lastModified: config.LastModified!,
|
|
236
|
+
version: config.Version!,
|
|
237
|
+
qualifiedArn: `${config.FunctionArn}:${config.Version}`,
|
|
238
|
+
invokeArn: `arn:aws:apigateway:${region}:lambda:path/2015-03-31/functions/${config.FunctionArn}/invocations`,
|
|
239
|
+
sourceCodeHash: config.CodeSha256!,
|
|
240
|
+
sourceCodeSize: config.CodeSize!,
|
|
241
|
+
ephemeralStorageSize: config.EphemeralStorage?.Size,
|
|
242
|
+
architectures: config.Architectures || [],
|
|
243
|
+
masterArn: config.MasterArn,
|
|
244
|
+
revisionId: config.RevisionId!,
|
|
245
|
+
state: config.State,
|
|
246
|
+
stateReason: config.StateReason,
|
|
247
|
+
stateReasonCode: config.StateReasonCode,
|
|
248
|
+
lastUpdateStatus: config.LastUpdateStatus,
|
|
249
|
+
lastUpdateStatusReason: config.LastUpdateStatusReason,
|
|
250
|
+
lastUpdateStatusReasonCode: config.LastUpdateStatusReasonCode,
|
|
251
|
+
packageType: config.PackageType!,
|
|
252
|
+
signingProfileVersionArn: config.SigningProfileVersionArn,
|
|
253
|
+
signingJobArn: config.SigningJobArn,
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
return output;
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
) {}
|
|
260
|
+
|
|
261
|
+
// Helper to wait for function to stabilize
|
|
262
|
+
async function waitForFunctionStabilization(
|
|
263
|
+
client: LambdaClient,
|
|
264
|
+
functionName: string,
|
|
265
|
+
) {
|
|
266
|
+
while (true) {
|
|
267
|
+
const config = await client.send(
|
|
268
|
+
new GetFunctionConfigurationCommand({
|
|
269
|
+
FunctionName: functionName,
|
|
270
|
+
}),
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
// Check if function is in a stable state
|
|
274
|
+
if (config.State === "Active" && config.LastUpdateStatus === "Successful") {
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// If there's a failure, throw an error
|
|
279
|
+
if (config.State === "Failed" || config.LastUpdateStatus === "Failed") {
|
|
280
|
+
throw new Error(
|
|
281
|
+
`Function failed to stabilize: ${config.StateReason || config.LastUpdateStatusReason}`,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Helper to zip the code
|
|
290
|
+
async function zipCode(filePath: string): Promise<Buffer> {
|
|
291
|
+
const fileContent = await fs.promises.readFile(filePath);
|
|
292
|
+
const fileName = path.basename(filePath);
|
|
293
|
+
|
|
294
|
+
// Create a zip buffer in memory
|
|
295
|
+
const zip = new (await import("jszip")).default();
|
|
296
|
+
zip.file(fileName, fileContent);
|
|
297
|
+
return zip.generateAsync({
|
|
298
|
+
type: "nodebuffer",
|
|
299
|
+
compression: "DEFLATE",
|
|
300
|
+
platform: "UNIX",
|
|
301
|
+
});
|
|
302
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AttachRolePolicyCommand,
|
|
3
|
+
CreatePolicyCommand,
|
|
4
|
+
CreatePolicyVersionCommand,
|
|
5
|
+
DeletePolicyCommand,
|
|
6
|
+
DeletePolicyVersionCommand,
|
|
7
|
+
DetachRolePolicyCommand,
|
|
8
|
+
GetPolicyCommand,
|
|
9
|
+
GetPolicyVersionCommand,
|
|
10
|
+
IAMClient,
|
|
11
|
+
ListPolicyVersionsCommand,
|
|
12
|
+
NoSuchEntityException,
|
|
13
|
+
} from "@aws-sdk/client-iam";
|
|
14
|
+
import { ignore } from "../../error";
|
|
15
|
+
import { Resource } from "../../resource";
|
|
16
|
+
|
|
17
|
+
// Type-safe policy document types
|
|
18
|
+
export type Effect = "Allow" | "Deny";
|
|
19
|
+
|
|
20
|
+
export interface PolicyStatement {
|
|
21
|
+
Sid?: string;
|
|
22
|
+
Effect: Effect;
|
|
23
|
+
Action: string | string[];
|
|
24
|
+
Resource?: string | string[];
|
|
25
|
+
Condition?: Record<string, Record<string, string | string[]>>;
|
|
26
|
+
Principal?: Record<string, string | string[]>;
|
|
27
|
+
NotPrincipal?: Record<string, string | string[]>;
|
|
28
|
+
NotAction?: string | string[];
|
|
29
|
+
NotResource?: string | string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface PolicyDocument {
|
|
33
|
+
Version: "2012-10-17";
|
|
34
|
+
Statement: PolicyStatement[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Policy resource
|
|
38
|
+
export interface PolicyProps {
|
|
39
|
+
policyName: string;
|
|
40
|
+
document: PolicyDocument;
|
|
41
|
+
description?: string;
|
|
42
|
+
path?: string;
|
|
43
|
+
tags?: Record<string, string>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface PolicyOutput extends PolicyProps {
|
|
47
|
+
id: string; // Same as policyName
|
|
48
|
+
arn: string;
|
|
49
|
+
defaultVersionId: string;
|
|
50
|
+
attachmentCount: number;
|
|
51
|
+
createDate: Date;
|
|
52
|
+
updateDate: Date;
|
|
53
|
+
isAttachable: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class Policy extends Resource(
|
|
57
|
+
"iam::Policy",
|
|
58
|
+
async (ctx, props: PolicyProps): Promise<PolicyOutput> => {
|
|
59
|
+
const client = new IAMClient({});
|
|
60
|
+
const policyArn = `arn:aws:iam::${process.env.AWS_ACCOUNT_ID}:policy${props.path || "/"}${props.policyName}`;
|
|
61
|
+
|
|
62
|
+
if (ctx.event === "delete") {
|
|
63
|
+
try {
|
|
64
|
+
// List and delete all non-default versions first
|
|
65
|
+
const versions = await client.send(
|
|
66
|
+
new ListPolicyVersionsCommand({
|
|
67
|
+
PolicyArn: policyArn,
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
for (const version of versions.Versions || []) {
|
|
72
|
+
if (!version.IsDefaultVersion) {
|
|
73
|
+
await client.send(
|
|
74
|
+
new DeletePolicyVersionCommand({
|
|
75
|
+
PolicyArn: policyArn,
|
|
76
|
+
VersionId: version.VersionId,
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Delete the policy
|
|
83
|
+
await client.send(
|
|
84
|
+
new DeletePolicyCommand({
|
|
85
|
+
PolicyArn: policyArn,
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
} catch (error: any) {
|
|
89
|
+
if (error.name !== NoSuchEntityException.name) {
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
...props,
|
|
95
|
+
id: props.policyName,
|
|
96
|
+
arn: policyArn,
|
|
97
|
+
defaultVersionId: "v1",
|
|
98
|
+
attachmentCount: 0,
|
|
99
|
+
createDate: new Date(),
|
|
100
|
+
updateDate: new Date(),
|
|
101
|
+
isAttachable: true,
|
|
102
|
+
};
|
|
103
|
+
} else {
|
|
104
|
+
try {
|
|
105
|
+
// Check if policy exists
|
|
106
|
+
const existingPolicy = await client.send(
|
|
107
|
+
new GetPolicyCommand({
|
|
108
|
+
PolicyArn: policyArn,
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// Get current policy version
|
|
113
|
+
const currentVersion = await client.send(
|
|
114
|
+
new GetPolicyVersionCommand({
|
|
115
|
+
PolicyArn: policyArn,
|
|
116
|
+
VersionId: existingPolicy.Policy!.DefaultVersionId!,
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const currentDocument = JSON.parse(
|
|
121
|
+
decodeURIComponent(currentVersion.PolicyVersion!.Document!),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// If policy document changed, create new version
|
|
125
|
+
if (
|
|
126
|
+
JSON.stringify(currentDocument) !== JSON.stringify(props.document)
|
|
127
|
+
) {
|
|
128
|
+
// List versions to check if we need to delete old ones
|
|
129
|
+
const versions = await client.send(
|
|
130
|
+
new ListPolicyVersionsCommand({
|
|
131
|
+
PolicyArn: policyArn,
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// Delete oldest version if we have 5 versions (maximum allowed)
|
|
136
|
+
if (versions.Versions?.length === 5) {
|
|
137
|
+
const oldestVersion = versions.Versions.sort(
|
|
138
|
+
(a, b) => a.CreateDate!.getTime() - b.CreateDate!.getTime(),
|
|
139
|
+
)[0];
|
|
140
|
+
|
|
141
|
+
if (!oldestVersion.IsDefaultVersion) {
|
|
142
|
+
await client.send(
|
|
143
|
+
new DeletePolicyVersionCommand({
|
|
144
|
+
PolicyArn: policyArn,
|
|
145
|
+
VersionId: oldestVersion.VersionId!,
|
|
146
|
+
}),
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Create new version
|
|
152
|
+
await client.send(
|
|
153
|
+
new CreatePolicyVersionCommand({
|
|
154
|
+
PolicyArn: policyArn,
|
|
155
|
+
PolicyDocument: JSON.stringify(props.document),
|
|
156
|
+
SetAsDefault: true,
|
|
157
|
+
}),
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const policy = await client.send(
|
|
162
|
+
new GetPolicyCommand({
|
|
163
|
+
PolicyArn: policyArn,
|
|
164
|
+
}),
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
...props,
|
|
169
|
+
id: props.policyName,
|
|
170
|
+
arn: policy.Policy!.Arn!,
|
|
171
|
+
defaultVersionId: policy.Policy!.DefaultVersionId!,
|
|
172
|
+
attachmentCount: policy.Policy!.AttachmentCount!,
|
|
173
|
+
createDate: policy.Policy!.CreateDate!,
|
|
174
|
+
updateDate: policy.Policy!.UpdateDate!,
|
|
175
|
+
isAttachable: policy.Policy!.IsAttachable!,
|
|
176
|
+
};
|
|
177
|
+
} catch (error: any) {
|
|
178
|
+
if (error.name === "NoSuchEntity") {
|
|
179
|
+
// Create new policy
|
|
180
|
+
const newPolicy = await client.send(
|
|
181
|
+
new CreatePolicyCommand({
|
|
182
|
+
PolicyName: props.policyName,
|
|
183
|
+
PolicyDocument: JSON.stringify(props.document),
|
|
184
|
+
Description: props.description,
|
|
185
|
+
Path: props.path,
|
|
186
|
+
Tags: props.tags
|
|
187
|
+
? Object.entries(props.tags).map(([Key, Value]) => ({
|
|
188
|
+
Key,
|
|
189
|
+
Value,
|
|
190
|
+
}))
|
|
191
|
+
: undefined,
|
|
192
|
+
}),
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
...props,
|
|
197
|
+
id: props.policyName,
|
|
198
|
+
arn: newPolicy.Policy!.Arn!,
|
|
199
|
+
defaultVersionId: newPolicy.Policy!.DefaultVersionId!,
|
|
200
|
+
attachmentCount: newPolicy.Policy!.AttachmentCount!,
|
|
201
|
+
createDate: newPolicy.Policy!.CreateDate!,
|
|
202
|
+
updateDate: newPolicy.Policy!.UpdateDate!,
|
|
203
|
+
isAttachable: newPolicy.Policy!.IsAttachable!,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
) {}
|
|
211
|
+
|
|
212
|
+
// PolicyAttachment resource
|
|
213
|
+
export interface PolicyAttachmentProps {
|
|
214
|
+
policyArn: string;
|
|
215
|
+
roleName: string;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export interface PolicyAttachmentOutput extends PolicyAttachmentProps {}
|
|
219
|
+
|
|
220
|
+
export class PolicyAttachment extends Resource(
|
|
221
|
+
"iam::PolicyAttachment",
|
|
222
|
+
async (ctx, props: PolicyAttachmentProps) => {
|
|
223
|
+
const client = new IAMClient({});
|
|
224
|
+
|
|
225
|
+
if (ctx.event === "delete") {
|
|
226
|
+
await ignore(NoSuchEntityException.name, () =>
|
|
227
|
+
client.send(
|
|
228
|
+
new DetachRolePolicyCommand({
|
|
229
|
+
PolicyArn: props.policyArn,
|
|
230
|
+
RoleName: props.roleName,
|
|
231
|
+
}),
|
|
232
|
+
),
|
|
233
|
+
);
|
|
234
|
+
} else {
|
|
235
|
+
await client.send(
|
|
236
|
+
new AttachRolePolicyCommand({
|
|
237
|
+
PolicyArn: props.policyArn,
|
|
238
|
+
RoleName: props.roleName,
|
|
239
|
+
}),
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return props;
|
|
244
|
+
},
|
|
245
|
+
) {}
|