alchemy 0.0.8 → 0.1.2
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 +355 -0
- package/package.json +24 -20
- 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/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,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
|
+
) {}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CreateQueueCommand,
|
|
3
|
+
DeleteQueueCommand,
|
|
4
|
+
GetQueueAttributesCommand,
|
|
5
|
+
GetQueueUrlCommand,
|
|
6
|
+
SQSClient,
|
|
7
|
+
} from "@aws-sdk/client-sqs";
|
|
8
|
+
import { Resource } from "../../resource";
|
|
9
|
+
|
|
10
|
+
export interface QueueProps {
|
|
11
|
+
queueName: string;
|
|
12
|
+
fifo?: boolean;
|
|
13
|
+
visibilityTimeout?: number;
|
|
14
|
+
messageRetentionPeriod?: number;
|
|
15
|
+
maximumMessageSize?: number;
|
|
16
|
+
delaySeconds?: number;
|
|
17
|
+
receiveMessageWaitTimeSeconds?: number;
|
|
18
|
+
contentBasedDeduplication?: boolean;
|
|
19
|
+
deduplicationScope?: "messageGroup" | "queue";
|
|
20
|
+
fifoThroughputLimit?: "perQueue" | "perMessageGroupId";
|
|
21
|
+
tags?: Record<string, string>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface QueueOutput extends QueueProps {
|
|
25
|
+
id: string; // Same as queueName
|
|
26
|
+
arn: string;
|
|
27
|
+
url: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class Queue extends Resource(
|
|
31
|
+
"sqs::Queue",
|
|
32
|
+
async (ctx, props: QueueProps) => {
|
|
33
|
+
const client = new SQSClient({});
|
|
34
|
+
const queueName = props.fifo ? `${props.queueName}.fifo` : props.queueName;
|
|
35
|
+
|
|
36
|
+
if (ctx.event === "delete") {
|
|
37
|
+
try {
|
|
38
|
+
// Get queue URL first
|
|
39
|
+
const urlResponse = await client.send(
|
|
40
|
+
new GetQueueUrlCommand({
|
|
41
|
+
QueueName: queueName,
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// Delete the queue
|
|
46
|
+
await client.send(
|
|
47
|
+
new DeleteQueueCommand({
|
|
48
|
+
QueueUrl: urlResponse.QueueUrl,
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// Wait for queue to be deleted
|
|
53
|
+
let queueDeleted = false;
|
|
54
|
+
while (!queueDeleted) {
|
|
55
|
+
try {
|
|
56
|
+
await client.send(
|
|
57
|
+
new GetQueueUrlCommand({
|
|
58
|
+
QueueName: queueName,
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
// If we get here, queue still exists
|
|
62
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
63
|
+
} catch (error: any) {
|
|
64
|
+
if (error.name === "QueueDoesNotExist") {
|
|
65
|
+
queueDeleted = true;
|
|
66
|
+
} else {
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} catch (error: any) {
|
|
72
|
+
if (error.name !== "QueueDoesNotExist") {
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
...props,
|
|
79
|
+
id: props.queueName,
|
|
80
|
+
arn: `arn:aws:sqs:${client.config.region}:${process.env.AWS_ACCOUNT_ID}:${queueName}`,
|
|
81
|
+
url: "",
|
|
82
|
+
};
|
|
83
|
+
} else {
|
|
84
|
+
// Create queue with attributes
|
|
85
|
+
const attributes: Record<string, string> = {};
|
|
86
|
+
|
|
87
|
+
if (props.visibilityTimeout !== undefined) {
|
|
88
|
+
attributes["VisibilityTimeout"] = props.visibilityTimeout.toString();
|
|
89
|
+
}
|
|
90
|
+
if (props.messageRetentionPeriod !== undefined) {
|
|
91
|
+
attributes["MessageRetentionPeriod"] =
|
|
92
|
+
props.messageRetentionPeriod.toString();
|
|
93
|
+
}
|
|
94
|
+
if (props.maximumMessageSize !== undefined) {
|
|
95
|
+
attributes["MaximumMessageSize"] = props.maximumMessageSize.toString();
|
|
96
|
+
}
|
|
97
|
+
if (props.delaySeconds !== undefined) {
|
|
98
|
+
attributes["DelaySeconds"] = props.delaySeconds.toString();
|
|
99
|
+
}
|
|
100
|
+
if (props.receiveMessageWaitTimeSeconds !== undefined) {
|
|
101
|
+
attributes["ReceiveMessageWaitTimeSeconds"] =
|
|
102
|
+
props.receiveMessageWaitTimeSeconds.toString();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// FIFO specific attributes
|
|
106
|
+
if (props.fifo) {
|
|
107
|
+
attributes["FifoQueue"] = "true";
|
|
108
|
+
if (props.contentBasedDeduplication) {
|
|
109
|
+
attributes["ContentBasedDeduplication"] = "true";
|
|
110
|
+
}
|
|
111
|
+
if (props.deduplicationScope) {
|
|
112
|
+
attributes["DeduplicationScope"] = props.deduplicationScope;
|
|
113
|
+
}
|
|
114
|
+
if (props.fifoThroughputLimit) {
|
|
115
|
+
attributes["FifoThroughputLimit"] = props.fifoThroughputLimit;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Convert tags to AWS format
|
|
120
|
+
const tags = props.tags
|
|
121
|
+
? Object.entries(props.tags).reduce(
|
|
122
|
+
(acc, [key, value]) => ({ ...acc, [key]: value }),
|
|
123
|
+
{},
|
|
124
|
+
)
|
|
125
|
+
: undefined;
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
// Create the queue
|
|
129
|
+
const createResponse = await client.send(
|
|
130
|
+
new CreateQueueCommand({
|
|
131
|
+
QueueName: queueName,
|
|
132
|
+
Attributes: attributes,
|
|
133
|
+
tags,
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// Get queue attributes
|
|
138
|
+
const attributesResponse = await client.send(
|
|
139
|
+
new GetQueueAttributesCommand({
|
|
140
|
+
QueueUrl: createResponse.QueueUrl,
|
|
141
|
+
AttributeNames: ["QueueArn"],
|
|
142
|
+
}),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
...props,
|
|
147
|
+
id: props.queueName,
|
|
148
|
+
arn: attributesResponse.Attributes!.QueueArn!,
|
|
149
|
+
url: createResponse.QueueUrl!,
|
|
150
|
+
};
|
|
151
|
+
} catch (error: any) {
|
|
152
|
+
if (error.name === "QueueAlreadyExists") {
|
|
153
|
+
// Get existing queue URL
|
|
154
|
+
const urlResponse = await client.send(
|
|
155
|
+
new GetQueueUrlCommand({
|
|
156
|
+
QueueName: queueName,
|
|
157
|
+
}),
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// Get queue attributes
|
|
161
|
+
const attributesResponse = await client.send(
|
|
162
|
+
new GetQueueAttributesCommand({
|
|
163
|
+
QueueUrl: urlResponse.QueueUrl,
|
|
164
|
+
AttributeNames: ["QueueArn"],
|
|
165
|
+
}),
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
...props,
|
|
170
|
+
id: props.queueName,
|
|
171
|
+
arn: attributesResponse.Attributes!.QueueArn!,
|
|
172
|
+
url: urlResponse.QueueUrl!,
|
|
173
|
+
};
|
|
174
|
+
} else if (error.name === "QueueDeletedRecently") {
|
|
175
|
+
// Queue was recently deleted, wait and retry
|
|
176
|
+
const maxRetries = 3;
|
|
177
|
+
let retryCount = 0;
|
|
178
|
+
|
|
179
|
+
while (retryCount < maxRetries) {
|
|
180
|
+
try {
|
|
181
|
+
// Wait for 60 seconds before retrying
|
|
182
|
+
await new Promise((resolve) => setTimeout(resolve, 61000));
|
|
183
|
+
|
|
184
|
+
// Retry creating the queue
|
|
185
|
+
const createResponse = await client.send(
|
|
186
|
+
new CreateQueueCommand({
|
|
187
|
+
QueueName: queueName,
|
|
188
|
+
Attributes: attributes,
|
|
189
|
+
tags,
|
|
190
|
+
}),
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// Get queue attributes
|
|
194
|
+
const attributesResponse = await client.send(
|
|
195
|
+
new GetQueueAttributesCommand({
|
|
196
|
+
QueueUrl: createResponse.QueueUrl,
|
|
197
|
+
AttributeNames: ["QueueArn"],
|
|
198
|
+
}),
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
...props,
|
|
203
|
+
id: props.queueName,
|
|
204
|
+
arn: attributesResponse.Attributes!.QueueArn!,
|
|
205
|
+
url: createResponse.QueueUrl!,
|
|
206
|
+
};
|
|
207
|
+
} catch (retryError: any) {
|
|
208
|
+
if (
|
|
209
|
+
retryError.name !== "QueueDeletedRecently" ||
|
|
210
|
+
retryCount === maxRetries - 1
|
|
211
|
+
) {
|
|
212
|
+
throw retryError;
|
|
213
|
+
}
|
|
214
|
+
retryCount++;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
) {}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CreateRoleCommand,
|
|
3
|
+
DeleteRoleCommand,
|
|
4
|
+
DeleteRolePolicyCommand,
|
|
5
|
+
EntityAlreadyExistsException,
|
|
6
|
+
GetRoleCommand,
|
|
7
|
+
IAMClient,
|
|
8
|
+
NoSuchEntityException,
|
|
9
|
+
PutRolePolicyCommand,
|
|
10
|
+
type Tag,
|
|
11
|
+
TagRoleCommand,
|
|
12
|
+
UpdateAssumeRolePolicyCommand,
|
|
13
|
+
UpdateRoleCommand,
|
|
14
|
+
} from "@aws-sdk/client-iam";
|
|
15
|
+
import { ignore } from "../../error";
|
|
16
|
+
import { type Context, Resource } from "../../resource";
|
|
17
|
+
import type { PolicyDocument } from "./policy";
|
|
18
|
+
|
|
19
|
+
export interface RoleProps {
|
|
20
|
+
roleName: string;
|
|
21
|
+
assumeRolePolicy: PolicyDocument;
|
|
22
|
+
description?: string;
|
|
23
|
+
path?: string;
|
|
24
|
+
maxSessionDuration?: number;
|
|
25
|
+
permissionsBoundary?: string;
|
|
26
|
+
policies?: Array<{
|
|
27
|
+
policyName: string;
|
|
28
|
+
policyDocument: PolicyDocument;
|
|
29
|
+
}>;
|
|
30
|
+
managedPolicyArns?: string[];
|
|
31
|
+
tags?: Record<string, string>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface RoleOutput extends RoleProps {
|
|
35
|
+
id: string; // Same as roleName
|
|
36
|
+
arn: string;
|
|
37
|
+
uniqueId: string; // Unique identifier for the role
|
|
38
|
+
roleId: string; // The stable and unique string identifying the role
|
|
39
|
+
createDate: Date;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class Role extends Resource(
|
|
43
|
+
"iam::Role",
|
|
44
|
+
async (ctx: Context<RoleOutput>, props: RoleProps): Promise<RoleOutput> => {
|
|
45
|
+
const client = new IAMClient({});
|
|
46
|
+
|
|
47
|
+
if (ctx.event === "delete") {
|
|
48
|
+
// Delete any inline policies first
|
|
49
|
+
if (props.policies) {
|
|
50
|
+
for (const policy of props.policies) {
|
|
51
|
+
await ignore(NoSuchEntityException.name, () =>
|
|
52
|
+
client.send(
|
|
53
|
+
new DeleteRolePolicyCommand({
|
|
54
|
+
RoleName: props.roleName,
|
|
55
|
+
PolicyName: policy.policyName,
|
|
56
|
+
}),
|
|
57
|
+
),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await ignore(NoSuchEntityException.name, () =>
|
|
63
|
+
client.send(
|
|
64
|
+
new DeleteRoleCommand({
|
|
65
|
+
RoleName: props.roleName,
|
|
66
|
+
}),
|
|
67
|
+
),
|
|
68
|
+
);
|
|
69
|
+
return {
|
|
70
|
+
...props,
|
|
71
|
+
id: props.roleName,
|
|
72
|
+
arn: `arn:aws:iam::${process.env.AWS_ACCOUNT_ID}:role/${props.roleName}`,
|
|
73
|
+
uniqueId: "",
|
|
74
|
+
roleId: "",
|
|
75
|
+
createDate: new Date(),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const assumeRolePolicyDocument = JSON.stringify(props.assumeRolePolicy);
|
|
80
|
+
let role;
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
if (ctx.event === "create") {
|
|
84
|
+
// Try to create the role
|
|
85
|
+
await client.send(
|
|
86
|
+
new CreateRoleCommand({
|
|
87
|
+
RoleName: props.roleName,
|
|
88
|
+
AssumeRolePolicyDocument: assumeRolePolicyDocument,
|
|
89
|
+
Description: props.description,
|
|
90
|
+
Path: props.path,
|
|
91
|
+
MaxSessionDuration: props.maxSessionDuration,
|
|
92
|
+
PermissionsBoundary: props.permissionsBoundary,
|
|
93
|
+
Tags: [
|
|
94
|
+
...Object.entries(props.tags || {}).map(([Key, Value]) => ({
|
|
95
|
+
Key,
|
|
96
|
+
Value,
|
|
97
|
+
})),
|
|
98
|
+
{
|
|
99
|
+
Key: "alchemy_stage",
|
|
100
|
+
Value: ctx.stage,
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
Key: "alchemy_resource",
|
|
104
|
+
Value: ctx.resourceID,
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
} catch (error: any) {
|
|
111
|
+
if (
|
|
112
|
+
error instanceof EntityAlreadyExistsException &&
|
|
113
|
+
ctx.event === "create"
|
|
114
|
+
) {
|
|
115
|
+
// Check if we were the ones who created it
|
|
116
|
+
const existingRole = await client.send(
|
|
117
|
+
new GetRoleCommand({
|
|
118
|
+
RoleName: props.roleName,
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
const roleTags =
|
|
122
|
+
existingRole.Role?.Tags?.reduce(
|
|
123
|
+
(acc, tag) => {
|
|
124
|
+
acc[tag.Key!] = tag.Value!;
|
|
125
|
+
return acc;
|
|
126
|
+
},
|
|
127
|
+
{} as Record<string, string>,
|
|
128
|
+
) || {};
|
|
129
|
+
|
|
130
|
+
if (
|
|
131
|
+
roleTags.alchemy_stage !== ctx.stage ||
|
|
132
|
+
roleTags.alchemy_resource !== ctx.resourceID
|
|
133
|
+
) {
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
} else if (error.name !== NoSuchEntityException.name) {
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Get or update the role
|
|
142
|
+
role = await client.send(
|
|
143
|
+
new GetRoleCommand({
|
|
144
|
+
RoleName: props.roleName,
|
|
145
|
+
}),
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// Update assume role policy if it changed
|
|
149
|
+
if (role.Role?.AssumeRolePolicyDocument !== assumeRolePolicyDocument) {
|
|
150
|
+
await client.send(
|
|
151
|
+
new UpdateAssumeRolePolicyCommand({
|
|
152
|
+
RoleName: props.roleName,
|
|
153
|
+
PolicyDocument: assumeRolePolicyDocument,
|
|
154
|
+
}),
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Update role description and max session duration if they changed
|
|
159
|
+
if (
|
|
160
|
+
role.Role?.Description !== props.description ||
|
|
161
|
+
role.Role?.MaxSessionDuration !== props.maxSessionDuration
|
|
162
|
+
) {
|
|
163
|
+
await client.send(
|
|
164
|
+
new UpdateRoleCommand({
|
|
165
|
+
RoleName: props.roleName,
|
|
166
|
+
Description: props.description,
|
|
167
|
+
MaxSessionDuration: props.maxSessionDuration,
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Update tags
|
|
173
|
+
const newTags = {
|
|
174
|
+
...props.tags,
|
|
175
|
+
alchemy_stage: ctx.stage,
|
|
176
|
+
alchemy_resource: ctx.resourceID,
|
|
177
|
+
};
|
|
178
|
+
const tags: Tag[] = Object.entries(newTags).map(([Key, Value]) => ({
|
|
179
|
+
Key,
|
|
180
|
+
Value,
|
|
181
|
+
}));
|
|
182
|
+
await client.send(
|
|
183
|
+
new TagRoleCommand({
|
|
184
|
+
RoleName: props.roleName,
|
|
185
|
+
Tags: tags,
|
|
186
|
+
}),
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
// Handle policy changes
|
|
190
|
+
const previousPolicies =
|
|
191
|
+
ctx.event === "update" ? ctx.output.policies || [] : [];
|
|
192
|
+
const currentPolicies = props.policies || [];
|
|
193
|
+
|
|
194
|
+
// Delete policies that were removed
|
|
195
|
+
for (const oldPolicy of previousPolicies) {
|
|
196
|
+
if (
|
|
197
|
+
!currentPolicies.some(
|
|
198
|
+
(p: { policyName: string }) => p.policyName === oldPolicy.policyName,
|
|
199
|
+
)
|
|
200
|
+
) {
|
|
201
|
+
await ignore(NoSuchEntityException.name, () =>
|
|
202
|
+
client.send(
|
|
203
|
+
new DeleteRolePolicyCommand({
|
|
204
|
+
RoleName: props.roleName,
|
|
205
|
+
PolicyName: oldPolicy.policyName,
|
|
206
|
+
}),
|
|
207
|
+
),
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Update or create policies
|
|
213
|
+
for (const policy of currentPolicies) {
|
|
214
|
+
const oldPolicy = previousPolicies.find(
|
|
215
|
+
(p) => p.policyName === policy.policyName,
|
|
216
|
+
);
|
|
217
|
+
if (
|
|
218
|
+
!oldPolicy ||
|
|
219
|
+
JSON.stringify(oldPolicy.policyDocument) !==
|
|
220
|
+
JSON.stringify(policy.policyDocument)
|
|
221
|
+
) {
|
|
222
|
+
await client.send(
|
|
223
|
+
new PutRolePolicyCommand({
|
|
224
|
+
RoleName: props.roleName,
|
|
225
|
+
PolicyName: policy.policyName,
|
|
226
|
+
PolicyDocument: JSON.stringify(policy.policyDocument),
|
|
227
|
+
}),
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!role?.Role) {
|
|
233
|
+
throw new Error(`Failed to create or update role ${props.roleName}`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
...props,
|
|
238
|
+
id: props.roleName,
|
|
239
|
+
arn: role.Role.Arn!,
|
|
240
|
+
uniqueId: role.Role.RoleId!,
|
|
241
|
+
roleId: role.Role.RoleId!,
|
|
242
|
+
createDate: role.Role.CreateDate!,
|
|
243
|
+
};
|
|
244
|
+
},
|
|
245
|
+
) {}
|