@action-llama/action-llama 0.10.1 → 0.10.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/dist/agents/execution-engine.d.ts.map +1 -1
- package/dist/agents/execution-engine.js +2 -15
- package/dist/agents/execution-engine.js.map +1 -1
- package/dist/agents/runner.d.ts.map +1 -1
- package/dist/agents/runner.js +2 -15
- package/dist/agents/runner.js.map +1 -1
- package/dist/cli/commands/cloud-iam.d.ts +17 -0
- package/dist/cli/commands/cloud-iam.d.ts.map +1 -0
- package/dist/cli/commands/cloud-iam.js +591 -0
- package/dist/cli/commands/cloud-iam.js.map +1 -0
- package/dist/cli/commands/cloud-setup-ecs.d.ts +9 -0
- package/dist/cli/commands/cloud-setup-ecs.d.ts.map +1 -0
- package/dist/cli/commands/cloud-setup-ecs.js +697 -0
- package/dist/cli/commands/cloud-setup-ecs.js.map +1 -0
- package/dist/cli/commands/cloud-setup.d.ts.map +1 -1
- package/dist/cli/commands/cloud-setup.js +2 -827
- package/dist/cli/commands/cloud-setup.js.map +1 -1
- package/dist/cli/commands/creds.d.ts.map +1 -1
- package/dist/cli/commands/creds.js +2 -4
- package/dist/cli/commands/creds.js.map +1 -1
- package/dist/cli/commands/doctor.d.ts +1 -3
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/doctor.js +60 -635
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/kill.d.ts.map +1 -1
- package/dist/cli/commands/kill.js +11 -14
- package/dist/cli/commands/kill.js.map +1 -1
- package/dist/cli/commands/pause.d.ts.map +1 -1
- package/dist/cli/commands/pause.js +11 -14
- package/dist/cli/commands/pause.js.map +1 -1
- package/dist/cli/commands/resume.d.ts.map +1 -1
- package/dist/cli/commands/resume.js +11 -14
- package/dist/cli/commands/resume.js.map +1 -1
- package/dist/cli/main.js +34 -32
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/with-command.d.ts +13 -0
- package/dist/cli/with-command.d.ts.map +1 -0
- package/dist/cli/with-command.js +45 -0
- package/dist/cli/with-command.js.map +1 -0
- package/dist/gateway/index.d.ts +1 -0
- package/dist/gateway/index.d.ts.map +1 -1
- package/dist/gateway/index.js +5 -2
- package/dist/gateway/index.js.map +1 -1
- package/dist/gateway/rate-limiter.d.ts +16 -0
- package/dist/gateway/rate-limiter.d.ts.map +1 -0
- package/dist/gateway/rate-limiter.js +38 -0
- package/dist/gateway/rate-limiter.js.map +1 -0
- package/dist/gateway/routes/dashboard.d.ts.map +1 -1
- package/dist/gateway/routes/dashboard.js +8 -0
- package/dist/gateway/routes/dashboard.js.map +1 -1
- package/dist/gateway/routes/locks.d.ts +3 -1
- package/dist/gateway/routes/locks.d.ts.map +1 -1
- package/dist/gateway/routes/locks.js +18 -13
- package/dist/gateway/routes/locks.js.map +1 -1
- package/dist/gateway/routes/webhooks.d.ts.map +1 -1
- package/dist/gateway/routes/webhooks.js +14 -0
- package/dist/gateway/routes/webhooks.js.map +1 -1
- package/dist/scheduler/index.d.ts +1 -2
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +22 -180
- package/dist/scheduler/index.js.map +1 -1
- package/dist/scheduler/runtime-factory.d.ts +24 -4
- package/dist/scheduler/runtime-factory.d.ts.map +1 -1
- package/dist/scheduler/runtime-factory.js +60 -64
- package/dist/scheduler/runtime-factory.js.map +1 -1
- package/dist/scheduler/webhook-setup.d.ts +11 -20
- package/dist/scheduler/webhook-setup.d.ts.map +1 -1
- package/dist/scheduler/webhook-setup.js +22 -68
- package/dist/scheduler/webhook-setup.js.map +1 -1
- package/dist/shared/config.d.ts +1 -0
- package/dist/shared/config.d.ts.map +1 -1
- package/dist/shared/config.js +13 -2
- package/dist/shared/config.js.map +1 -1
- package/dist/shared/credentials.d.ts.map +1 -1
- package/dist/shared/credentials.js +2 -1
- package/dist/shared/credentials.js.map +1 -1
- package/dist/shared/errors.d.ts +34 -0
- package/dist/shared/errors.d.ts.map +1 -0
- package/dist/shared/errors.js +47 -0
- package/dist/shared/errors.js.map +1 -0
- package/dist/shared/git.d.ts.map +1 -1
- package/dist/shared/git.js +3 -2
- package/dist/shared/git.js.map +1 -1
- package/dist/webhooks/providers/github.d.ts.map +1 -1
- package/dist/webhooks/providers/github.js +2 -21
- package/dist/webhooks/providers/github.js.map +1 -1
- package/dist/webhooks/providers/linear.d.ts.map +1 -1
- package/dist/webhooks/providers/linear.js +2 -21
- package/dist/webhooks/providers/linear.js.map +1 -1
- package/dist/webhooks/providers/sentry.d.ts.map +1 -1
- package/dist/webhooks/providers/sentry.js +2 -20
- package/dist/webhooks/providers/sentry.js.map +1 -1
- package/dist/webhooks/validation.d.ts +17 -0
- package/dist/webhooks/validation.d.ts.map +1 -0
- package/dist/webhooks/validation.js +37 -0
- package/dist/webhooks/validation.js.map +1 -0
- package/package.json +1 -1
- package/dist/scheduler/config-validator.d.ts +0 -18
- package/dist/scheduler/config-validator.d.ts.map +0 -1
- package/dist/scheduler/config-validator.js +0 -53
- package/dist/scheduler/config-validator.js.map +0 -1
- package/dist/scheduler/cron-manager.d.ts +0 -14
- package/dist/scheduler/cron-manager.d.ts.map +0 -1
- package/dist/scheduler/cron-manager.js +0 -75
- package/dist/scheduler/cron-manager.js.map +0 -1
- package/dist/scheduler/shutdown-handler.d.ts +0 -16
- package/dist/scheduler/shutdown-handler.d.ts.map +0 -1
- package/dist/scheduler/shutdown-handler.js +0 -44
- package/dist/scheduler/shutdown-handler.js.map +0 -1
- package/dist/scheduler/trigger-dispatcher.d.ts +0 -12
- package/dist/scheduler/trigger-dispatcher.d.ts.map +0 -1
- package/dist/scheduler/trigger-dispatcher.js +0 -46
- package/dist/scheduler/trigger-dispatcher.js.map +0 -1
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ECS-specific cloud setup logic.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from cloud-setup.ts to keep the main orchestrator slim.
|
|
5
|
+
* Contains all AWS resource creation/discovery for ECS Fargate setup.
|
|
6
|
+
*/
|
|
7
|
+
import { select, input, confirm } from "@inquirer/prompts";
|
|
8
|
+
import { STSClient, GetCallerIdentityCommand } from "@aws-sdk/client-sts";
|
|
9
|
+
import { ECRClient, DescribeRepositoriesCommand, CreateRepositoryCommand, SetRepositoryPolicyCommand, } from "@aws-sdk/client-ecr";
|
|
10
|
+
import { ECSClient, ListClustersCommand, DescribeClustersCommand, CreateClusterCommand, } from "@aws-sdk/client-ecs";
|
|
11
|
+
import { IAMClient, ListRolesCommand, CreateRoleCommand, GetRoleCommand, AttachRolePolicyCommand, PutRolePolicyCommand, PutUserPolicyCommand, CreateServiceLinkedRoleCommand, } from "@aws-sdk/client-iam";
|
|
12
|
+
import { EC2Client, DescribeVpcsCommand, DescribeSubnetsCommand, DescribeSecurityGroupsCommand, } from "@aws-sdk/client-ec2";
|
|
13
|
+
import { CloudWatchLogsClient, CreateLogGroupCommand, } from "@aws-sdk/client-cloudwatch-logs";
|
|
14
|
+
import { AWS_CONSTANTS } from "../../shared/aws-constants.js";
|
|
15
|
+
const CREATE_NEW = "__create_new__";
|
|
16
|
+
const MANUAL_INPUT = "__manual_input__";
|
|
17
|
+
export async function setupEcsCloud(cloud) {
|
|
18
|
+
// Check for AWS credentials before asking any questions
|
|
19
|
+
console.log("Checking for AWS credentials...");
|
|
20
|
+
const probe = new STSClient({});
|
|
21
|
+
try {
|
|
22
|
+
const identity = await probe.send(new GetCallerIdentityCommand({}));
|
|
23
|
+
console.log(` Authenticated as ${identity.Arn}\n`);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
console.log("\n No AWS credentials found. To configure them:\n");
|
|
27
|
+
console.log(" 1. Install the AWS CLI:");
|
|
28
|
+
console.log(" https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html\n");
|
|
29
|
+
console.log(" 2. Run: aws configure\n");
|
|
30
|
+
console.log(" 3. Then re-run: al cloud setup\n");
|
|
31
|
+
console.log(" Alternatively, set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables.\n");
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
cloud.awsRegion = await input({ message: "AWS region:", default: "us-east-1" });
|
|
35
|
+
const region = cloud.awsRegion;
|
|
36
|
+
// Create SDK clients with the chosen region
|
|
37
|
+
const stsClient = new STSClient({ region });
|
|
38
|
+
const ecrClient = new ECRClient({ region });
|
|
39
|
+
const ecsClient = new ECSClient({ region });
|
|
40
|
+
const iamClient = new IAMClient({});
|
|
41
|
+
const ec2Client = new EC2Client({ region });
|
|
42
|
+
// Get account ID in the target region
|
|
43
|
+
const identity = await stsClient.send(new GetCallerIdentityCommand({}));
|
|
44
|
+
const accountId = identity.Account;
|
|
45
|
+
// Ensure service-linked roles exist (one-time per AWS account)
|
|
46
|
+
await ensureServiceLinkedRoles(iamClient);
|
|
47
|
+
cloud.ecrRepository = await pickOrCreateEcrRepo(ecrClient, region, accountId);
|
|
48
|
+
await ensureLambdaEcrPolicy(ecrClient, cloud.ecrRepository);
|
|
49
|
+
cloud.ecsCluster = await pickOrCreateEcsCluster(ecsClient);
|
|
50
|
+
cloud.executionRoleArn = await pickOrCreateEcsRole(iamClient, "Execution role (ECR pull + CloudWatch Logs)", AWS_CONSTANTS.EXECUTION_ROLE, ["arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"]);
|
|
51
|
+
cloud.taskRoleArn = await pickOrCreateEcsRole(iamClient, "Default task role (Secrets Manager access)", AWS_CONSTANTS.DEFAULT_TASK_ROLE, [], [cloud.executionRoleArn]);
|
|
52
|
+
// Add Secrets Manager + CloudWatch Logs inline policy to execution role
|
|
53
|
+
const executionRoleName = cloud.executionRoleArn.split("/").pop();
|
|
54
|
+
const secretPrefix = AWS_CONSTANTS.DEFAULT_SECRET_PREFIX;
|
|
55
|
+
try {
|
|
56
|
+
await iamClient.send(new PutRolePolicyCommand({
|
|
57
|
+
RoleName: executionRoleName,
|
|
58
|
+
PolicyName: "ActionLlamaExecution",
|
|
59
|
+
PolicyDocument: JSON.stringify({
|
|
60
|
+
Version: "2012-10-17",
|
|
61
|
+
Statement: [
|
|
62
|
+
{
|
|
63
|
+
Effect: "Allow",
|
|
64
|
+
Action: "secretsmanager:GetSecretValue",
|
|
65
|
+
Resource: `arn:aws:secretsmanager:${region}:${accountId}:secret:${secretPrefix}/*`,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
Effect: "Allow",
|
|
69
|
+
Action: "logs:CreateLogGroup",
|
|
70
|
+
Resource: `arn:aws:logs:${region}:${accountId}:log-group:${AWS_CONSTANTS.LOG_GROUP}*`,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
}),
|
|
74
|
+
}));
|
|
75
|
+
console.log(` Attached ActionLlamaExecution policy to ${executionRoleName}`);
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
throw new Error(`Failed to attach ActionLlamaExecution policy to ${executionRoleName}: ${err.message}\n` +
|
|
79
|
+
`The execution role needs secretsmanager:GetSecretValue and logs:CreateLogGroup permissions.\n` +
|
|
80
|
+
`Either grant your IAM user iam:PutRolePolicy on this role, or attach the policy manually in the AWS Console.`);
|
|
81
|
+
}
|
|
82
|
+
// Create CloudWatch log group for ECS task logs
|
|
83
|
+
const cwlClient = new CloudWatchLogsClient({ region });
|
|
84
|
+
try {
|
|
85
|
+
await cwlClient.send(new CreateLogGroupCommand({ logGroupName: AWS_CONSTANTS.LOG_GROUP }));
|
|
86
|
+
console.log(` Created log group: ${AWS_CONSTANTS.LOG_GROUP}`);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
if (err.name === "ResourceAlreadyExistsException") {
|
|
90
|
+
console.log(` Log group already exists: ${AWS_CONSTANTS.LOG_GROUP}`);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
console.log(` Warning: could not create log group: ${err.message}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Create CodeBuild service role for remote image builds
|
|
97
|
+
await ensureCodeBuildRole(iamClient, accountId, region, cloud.ecrRepository);
|
|
98
|
+
// Create App Runner roles for cloud deploy
|
|
99
|
+
cloud.appRunnerAccessRoleArn = await ensureAppRunnerAccessRole(iamClient);
|
|
100
|
+
cloud.appRunnerInstanceRoleArn = await ensureAppRunnerInstanceRole(iamClient, accountId, region, cloud.ecrRepository);
|
|
101
|
+
const result = await pickVpcAndSubnets(ec2Client);
|
|
102
|
+
cloud.subnets = result.subnets;
|
|
103
|
+
const sgs = await pickSecurityGroups(ec2Client, result.vpcId);
|
|
104
|
+
if (sgs.length > 0)
|
|
105
|
+
cloud.securityGroups = sgs;
|
|
106
|
+
const prefix = await input({ message: "Secret prefix:", default: AWS_CONSTANTS.DEFAULT_SECRET_PREFIX });
|
|
107
|
+
if (prefix !== AWS_CONSTANTS.DEFAULT_SECRET_PREFIX)
|
|
108
|
+
cloud.awsSecretPrefix = prefix;
|
|
109
|
+
// Grant iam:PassRole, logs read, and iam:PutUserPolicy to the calling
|
|
110
|
+
// IAM user so that al start/run can assign roles, al logs can read
|
|
111
|
+
// CloudWatch, and al doctor -c can update this policy later.
|
|
112
|
+
const callerArn = identity.Arn;
|
|
113
|
+
const userMatch = callerArn.match(/:user\/(.+)$/);
|
|
114
|
+
if (userMatch) {
|
|
115
|
+
const userName = userMatch[1];
|
|
116
|
+
const operatorPolicy = JSON.stringify({
|
|
117
|
+
Version: "2012-10-17",
|
|
118
|
+
Statement: [
|
|
119
|
+
{
|
|
120
|
+
Effect: "Allow",
|
|
121
|
+
Action: "iam:PassRole",
|
|
122
|
+
Resource: `arn:aws:iam::${accountId}:role/al-*`,
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
Effect: "Allow",
|
|
126
|
+
Action: [
|
|
127
|
+
"logs:CreateLogGroup",
|
|
128
|
+
"logs:GetLogEvents",
|
|
129
|
+
"logs:FilterLogEvents",
|
|
130
|
+
],
|
|
131
|
+
Resource: [
|
|
132
|
+
`arn:aws:logs:${region}:${accountId}:log-group:${AWS_CONSTANTS.LOG_GROUP}*`,
|
|
133
|
+
`arn:aws:logs:${region}:${accountId}:log-group:${AWS_CONSTANTS.LAMBDA_LOG_GROUP}/al-*`,
|
|
134
|
+
`arn:aws:logs:${region}:${accountId}:log-group:${AWS_CONSTANTS.APPRUNNER_LOG_GROUP}*`,
|
|
135
|
+
],
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
Effect: "Allow",
|
|
139
|
+
Action: [
|
|
140
|
+
"apprunner:CreateService",
|
|
141
|
+
"apprunner:UpdateService",
|
|
142
|
+
"apprunner:DescribeService",
|
|
143
|
+
"apprunner:DeleteService",
|
|
144
|
+
],
|
|
145
|
+
Resource: `arn:aws:apprunner:${region}:${accountId}:service/al-scheduler/*`,
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
Effect: "Allow",
|
|
149
|
+
Action: "apprunner:ListServices",
|
|
150
|
+
Resource: "*",
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
Effect: "Allow",
|
|
154
|
+
Action: "iam:PutUserPolicy",
|
|
155
|
+
Resource: `arn:aws:iam::${accountId}:user/${userName}`,
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
Effect: "Allow",
|
|
159
|
+
Action: "iam:CreateServiceLinkedRole",
|
|
160
|
+
Resource: [
|
|
161
|
+
`arn:aws:iam::${accountId}:role/aws-service-role/ecs.amazonaws.com/*`,
|
|
162
|
+
`arn:aws:iam::${accountId}:role/aws-service-role/apprunner.amazonaws.com/*`,
|
|
163
|
+
],
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
});
|
|
167
|
+
try {
|
|
168
|
+
await iamClient.send(new PutUserPolicyCommand({
|
|
169
|
+
UserName: userName,
|
|
170
|
+
PolicyName: "ActionLlamaOperator",
|
|
171
|
+
PolicyDocument: operatorPolicy,
|
|
172
|
+
}));
|
|
173
|
+
console.log(` Granted iam:PassRole + logs read permissions to user ${userName}`);
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
console.log(`\n Warning: could not auto-grant operator permissions to user ${userName}: ${err.message}`);
|
|
177
|
+
console.log(` You must manually attach the ActionLlamaOperator policy to user "${userName}" in the AWS Console.`);
|
|
178
|
+
console.log(` See docs/ecs.md "Operator IAM policy" for the full policy document.`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
// --- Service-linked roles ---
|
|
184
|
+
async function ensureServiceLinkedRoles(iamClient) {
|
|
185
|
+
const services = [
|
|
186
|
+
{ name: "ECS", serviceName: "ecs.amazonaws.com" },
|
|
187
|
+
{ name: "App Runner", serviceName: "apprunner.amazonaws.com" },
|
|
188
|
+
];
|
|
189
|
+
for (const svc of services) {
|
|
190
|
+
try {
|
|
191
|
+
await iamClient.send(new CreateServiceLinkedRoleCommand({
|
|
192
|
+
AWSServiceName: svc.serviceName,
|
|
193
|
+
}));
|
|
194
|
+
console.log(` Created service-linked role for ${svc.name}`);
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
if (err.name === "InvalidInputException" && err.message?.includes("already exists")) {
|
|
198
|
+
console.log(` Service-linked role for ${svc.name} already exists`);
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
console.log(` Warning: could not create service-linked role for ${svc.name}: ${err.message}`);
|
|
202
|
+
console.log(` You may need to run: aws iam create-service-linked-role --aws-service-name ${svc.serviceName}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// --- Resource pickers ---
|
|
208
|
+
async function pickOrCreateEcrRepo(ecrClient, region, accountId) {
|
|
209
|
+
console.log("Looking for ECR repositories...");
|
|
210
|
+
try {
|
|
211
|
+
const data = await ecrClient.send(new DescribeRepositoriesCommand({}));
|
|
212
|
+
const repos = data.repositories || [];
|
|
213
|
+
if (repos.length > 0) {
|
|
214
|
+
const choices = [
|
|
215
|
+
...repos.map((r) => ({ name: r.repositoryName, value: r.repositoryUri })),
|
|
216
|
+
{ name: "Create new repository", value: CREATE_NEW },
|
|
217
|
+
{ name: "Enter URI manually", value: MANUAL_INPUT },
|
|
218
|
+
];
|
|
219
|
+
const choice = await select({ message: "ECR repository:", choices });
|
|
220
|
+
if (choice === MANUAL_INPUT)
|
|
221
|
+
return input({ message: "ECR repository URI:" });
|
|
222
|
+
if (choice !== CREATE_NEW)
|
|
223
|
+
return choice;
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
console.log(" No ECR repositories found.");
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
console.log(" Could not list ECR repositories.");
|
|
231
|
+
}
|
|
232
|
+
const name = await input({ message: "New ECR repository name:", default: AWS_CONSTANTS.DEFAULT_ECR_REPO });
|
|
233
|
+
try {
|
|
234
|
+
const data = await ecrClient.send(new CreateRepositoryCommand({ repositoryName: name }));
|
|
235
|
+
const uri = data.repository.repositoryUri;
|
|
236
|
+
console.log(` Created: ${uri}`);
|
|
237
|
+
return uri;
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
if (err.name === "RepositoryAlreadyExistsException") {
|
|
241
|
+
const uri = `${accountId}.dkr.ecr.${region}.amazonaws.com/${name}`;
|
|
242
|
+
console.log(` Already exists: ${uri}`);
|
|
243
|
+
return uri;
|
|
244
|
+
}
|
|
245
|
+
console.log(` Failed to create repository: ${err.message}`);
|
|
246
|
+
return input({ message: "ECR repository URI:" });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
async function ensureLambdaEcrPolicy(ecrClient, ecrRepoUri) {
|
|
250
|
+
const repoName = ecrRepoUri.split("/").pop();
|
|
251
|
+
if (!repoName)
|
|
252
|
+
return;
|
|
253
|
+
const policy = JSON.stringify({
|
|
254
|
+
Version: "2012-10-17",
|
|
255
|
+
Statement: [{
|
|
256
|
+
Sid: "LambdaECRImageRetrievalPolicy",
|
|
257
|
+
Effect: "Allow",
|
|
258
|
+
Principal: { Service: "lambda.amazonaws.com" },
|
|
259
|
+
Action: [
|
|
260
|
+
"ecr:BatchGetImage",
|
|
261
|
+
"ecr:GetDownloadUrlForLayer",
|
|
262
|
+
],
|
|
263
|
+
}],
|
|
264
|
+
});
|
|
265
|
+
try {
|
|
266
|
+
await ecrClient.send(new SetRepositoryPolicyCommand({
|
|
267
|
+
repositoryName: repoName,
|
|
268
|
+
policyText: policy,
|
|
269
|
+
}));
|
|
270
|
+
console.log(` ECR repository policy: granted Lambda pull access`);
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
console.log(` Warning: could not set ECR repository policy for Lambda: ${err.message}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
async function pickOrCreateEcsCluster(ecsClient) {
|
|
277
|
+
console.log("\nLooking for ECS clusters...");
|
|
278
|
+
try {
|
|
279
|
+
const listData = await ecsClient.send(new ListClustersCommand({}));
|
|
280
|
+
const arns = listData.clusterArns || [];
|
|
281
|
+
if (arns.length > 0) {
|
|
282
|
+
const descData = await ecsClient.send(new DescribeClustersCommand({ clusters: arns }));
|
|
283
|
+
const clusters = (descData.clusters || []).filter((c) => c.status === "ACTIVE");
|
|
284
|
+
if (clusters.length > 0) {
|
|
285
|
+
const choices = [
|
|
286
|
+
...clusters.map((c) => ({
|
|
287
|
+
name: `${c.clusterName} (${c.runningTasksCount || 0} running tasks)`,
|
|
288
|
+
value: c.clusterName,
|
|
289
|
+
})),
|
|
290
|
+
{ name: "Create new cluster", value: CREATE_NEW },
|
|
291
|
+
{ name: "Enter name manually", value: MANUAL_INPUT },
|
|
292
|
+
];
|
|
293
|
+
const choice = await select({ message: "ECS cluster:", choices });
|
|
294
|
+
if (choice === MANUAL_INPUT)
|
|
295
|
+
return input({ message: "ECS cluster name:" });
|
|
296
|
+
if (choice !== CREATE_NEW)
|
|
297
|
+
return choice;
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
console.log(" No active ECS clusters found.");
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
console.log(" No ECS clusters found.");
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
console.log(" Could not list ECS clusters.");
|
|
309
|
+
}
|
|
310
|
+
const name = await input({ message: "New ECS cluster name:", default: AWS_CONSTANTS.DEFAULT_CLUSTER });
|
|
311
|
+
try {
|
|
312
|
+
await ecsClient.send(new CreateClusterCommand({ clusterName: name }));
|
|
313
|
+
console.log(` Created cluster: ${name}`);
|
|
314
|
+
return name;
|
|
315
|
+
}
|
|
316
|
+
catch (err) {
|
|
317
|
+
console.log(` Failed to create cluster: ${err.message}`);
|
|
318
|
+
return input({ message: "ECS cluster name:" });
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const ECS_TRUST_POLICY = JSON.stringify({
|
|
322
|
+
Version: "2012-10-17",
|
|
323
|
+
Statement: [{
|
|
324
|
+
Effect: "Allow",
|
|
325
|
+
Principal: { Service: "ecs-tasks.amazonaws.com" },
|
|
326
|
+
Action: "sts:AssumeRole",
|
|
327
|
+
}],
|
|
328
|
+
});
|
|
329
|
+
async function pickOrCreateEcsRole(iamClient, label, defaultName, managedPolicies, excludeArns) {
|
|
330
|
+
console.log(`\nLooking for IAM roles (${label})...`);
|
|
331
|
+
try {
|
|
332
|
+
const data = await iamClient.send(new ListRolesCommand({ MaxItems: 200 }));
|
|
333
|
+
const ecsRoles = (data.Roles || []).filter((r) => {
|
|
334
|
+
if (excludeArns?.includes(r.Arn))
|
|
335
|
+
return false;
|
|
336
|
+
try {
|
|
337
|
+
const doc = typeof r.AssumeRolePolicyDocument === "string"
|
|
338
|
+
? JSON.parse(decodeURIComponent(r.AssumeRolePolicyDocument))
|
|
339
|
+
: r.AssumeRolePolicyDocument;
|
|
340
|
+
return doc?.Statement?.some((s) => {
|
|
341
|
+
const svc = s.Principal?.Service;
|
|
342
|
+
return svc === "ecs-tasks.amazonaws.com" ||
|
|
343
|
+
(Array.isArray(svc) && svc.includes("ecs-tasks.amazonaws.com"));
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
if (ecsRoles.length > 0) {
|
|
351
|
+
// Sort so the expected default role appears first
|
|
352
|
+
const sorted = [...ecsRoles].sort((a, b) => {
|
|
353
|
+
const aMatch = a.RoleName === defaultName ? 0 : 1;
|
|
354
|
+
const bMatch = b.RoleName === defaultName ? 0 : 1;
|
|
355
|
+
return aMatch - bMatch || a.RoleName.localeCompare(b.RoleName);
|
|
356
|
+
});
|
|
357
|
+
const defaultArn = sorted.find((r) => r.RoleName === defaultName)?.Arn;
|
|
358
|
+
const choices = [
|
|
359
|
+
...sorted.map((r) => ({ name: r.RoleName, value: r.Arn })),
|
|
360
|
+
{ name: `Create new: ${defaultName}`, value: CREATE_NEW },
|
|
361
|
+
{ name: "Enter ARN manually", value: MANUAL_INPUT },
|
|
362
|
+
];
|
|
363
|
+
const choice = await select({ message: `${label}:`, choices, default: defaultArn });
|
|
364
|
+
if (choice === MANUAL_INPUT)
|
|
365
|
+
return input({ message: `${label} ARN:` });
|
|
366
|
+
if (choice !== CREATE_NEW)
|
|
367
|
+
return choice;
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
console.log(" No ECS-compatible IAM roles found.");
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
// iam:ListRoles not available — try to find the default role directly
|
|
375
|
+
try {
|
|
376
|
+
const data = await iamClient.send(new GetRoleCommand({ RoleName: defaultName }));
|
|
377
|
+
const arn = data.Role.Arn;
|
|
378
|
+
console.log(` Found existing role: ${arn}`);
|
|
379
|
+
const choices = [
|
|
380
|
+
{ name: `Use ${defaultName}`, value: arn },
|
|
381
|
+
{ name: "Enter ARN manually", value: MANUAL_INPUT },
|
|
382
|
+
];
|
|
383
|
+
const choice = await select({ message: `${label}:`, choices });
|
|
384
|
+
if (choice === MANUAL_INPUT)
|
|
385
|
+
return input({ message: `${label} ARN:` });
|
|
386
|
+
return choice;
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
console.log(" No existing role found.");
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const name = await input({ message: "New role name:", default: defaultName });
|
|
393
|
+
// Check if the role already exists before trying to create
|
|
394
|
+
try {
|
|
395
|
+
const data = await iamClient.send(new GetRoleCommand({ RoleName: name }));
|
|
396
|
+
const arn = data.Role.Arn;
|
|
397
|
+
console.log(` Role already exists: ${arn}`);
|
|
398
|
+
return arn;
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
// Role doesn't exist — create it
|
|
402
|
+
}
|
|
403
|
+
try {
|
|
404
|
+
const data = await iamClient.send(new CreateRoleCommand({
|
|
405
|
+
RoleName: name,
|
|
406
|
+
AssumeRolePolicyDocument: ECS_TRUST_POLICY,
|
|
407
|
+
}));
|
|
408
|
+
const arn = data.Role.Arn;
|
|
409
|
+
console.log(` Created role: ${arn}`);
|
|
410
|
+
for (const policyArn of managedPolicies) {
|
|
411
|
+
try {
|
|
412
|
+
await iamClient.send(new AttachRolePolicyCommand({
|
|
413
|
+
RoleName: name,
|
|
414
|
+
PolicyArn: policyArn,
|
|
415
|
+
}));
|
|
416
|
+
console.log(` Attached: ${policyArn.split("/").pop()}`);
|
|
417
|
+
}
|
|
418
|
+
catch (attachErr) {
|
|
419
|
+
console.log(` Warning: could not attach ${policyArn.split("/").pop()}: ${attachErr.message}`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return arn;
|
|
423
|
+
}
|
|
424
|
+
catch (err) {
|
|
425
|
+
console.log(` Failed to create role: ${err.message}`);
|
|
426
|
+
return input({ message: `${label} ARN:` });
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
async function ensureCodeBuildRole(iamClient, accountId, region, ecrRepository) {
|
|
430
|
+
const roleName = AWS_CONSTANTS.CODEBUILD_ROLE;
|
|
431
|
+
console.log(`\nEnsuring CodeBuild service role (${roleName})...`);
|
|
432
|
+
const trustPolicy = JSON.stringify({
|
|
433
|
+
Version: "2012-10-17",
|
|
434
|
+
Statement: [{
|
|
435
|
+
Effect: "Allow",
|
|
436
|
+
Principal: { Service: "codebuild.amazonaws.com" },
|
|
437
|
+
Action: "sts:AssumeRole",
|
|
438
|
+
}],
|
|
439
|
+
});
|
|
440
|
+
try {
|
|
441
|
+
await iamClient.send(new CreateRoleCommand({
|
|
442
|
+
RoleName: roleName,
|
|
443
|
+
AssumeRolePolicyDocument: trustPolicy,
|
|
444
|
+
}));
|
|
445
|
+
console.log(` Created role: ${roleName}`);
|
|
446
|
+
}
|
|
447
|
+
catch (err) {
|
|
448
|
+
if (err.name === "EntityAlreadyExistsException") {
|
|
449
|
+
console.log(` Role already exists`);
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
console.log(` Warning: could not create ${roleName}: ${err.message}`);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
// ECR push + S3 read + CloudWatch Logs
|
|
457
|
+
const repoArn = `arn:aws:ecr:${region}:${accountId}:repository/${ecrRepository.split("/").pop()}`;
|
|
458
|
+
const bucketName = AWS_CONSTANTS.buildBucket(accountId, region);
|
|
459
|
+
try {
|
|
460
|
+
await iamClient.send(new PutRolePolicyCommand({
|
|
461
|
+
RoleName: roleName,
|
|
462
|
+
PolicyName: "CodeBuildPermissions",
|
|
463
|
+
PolicyDocument: JSON.stringify({
|
|
464
|
+
Version: "2012-10-17",
|
|
465
|
+
Statement: [
|
|
466
|
+
{
|
|
467
|
+
Effect: "Allow",
|
|
468
|
+
Action: "ecr:GetAuthorizationToken",
|
|
469
|
+
Resource: "*",
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
Effect: "Allow",
|
|
473
|
+
Action: [
|
|
474
|
+
"ecr:BatchCheckLayerAvailability",
|
|
475
|
+
"ecr:PutImage",
|
|
476
|
+
"ecr:InitiateLayerUpload",
|
|
477
|
+
"ecr:UploadLayerPart",
|
|
478
|
+
"ecr:CompleteLayerUpload",
|
|
479
|
+
"ecr:GetDownloadUrlForLayer",
|
|
480
|
+
"ecr:BatchGetImage",
|
|
481
|
+
],
|
|
482
|
+
Resource: repoArn,
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
Effect: "Allow",
|
|
486
|
+
Action: "s3:GetObject",
|
|
487
|
+
Resource: `arn:aws:s3:::${bucketName}/*`,
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
Effect: "Allow",
|
|
491
|
+
Action: [
|
|
492
|
+
"logs:CreateLogGroup",
|
|
493
|
+
"logs:CreateLogStream",
|
|
494
|
+
"logs:PutLogEvents",
|
|
495
|
+
],
|
|
496
|
+
Resource: "*",
|
|
497
|
+
},
|
|
498
|
+
],
|
|
499
|
+
}),
|
|
500
|
+
}));
|
|
501
|
+
console.log(` Attached CodeBuildPermissions policy`);
|
|
502
|
+
}
|
|
503
|
+
catch (err) {
|
|
504
|
+
console.log(` Warning: could not attach policy to ${roleName}: ${err.message}`);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
async function pickVpcAndSubnets(ec2Client) {
|
|
508
|
+
console.log("\nLooking for VPCs...");
|
|
509
|
+
let vpcId;
|
|
510
|
+
try {
|
|
511
|
+
const data = await ec2Client.send(new DescribeVpcsCommand({}));
|
|
512
|
+
const vpcs = data.Vpcs || [];
|
|
513
|
+
if (vpcs.length > 0) {
|
|
514
|
+
if (vpcs.length === 1) {
|
|
515
|
+
vpcId = vpcs[0].VpcId;
|
|
516
|
+
const nameTag = vpcs[0].Tags?.find((t) => t.Key === "Name")?.Value;
|
|
517
|
+
console.log(` Using VPC: ${nameTag ? `${nameTag} (${vpcId})` : vpcId}`);
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
const choices = vpcs.map((v) => {
|
|
521
|
+
const nameTag = v.Tags?.find((t) => t.Key === "Name")?.Value;
|
|
522
|
+
const label = nameTag ? `${nameTag} (${v.VpcId})` : v.VpcId;
|
|
523
|
+
const defaultMarker = v.IsDefault ? " [default]" : "";
|
|
524
|
+
return { name: `${label} — ${v.CidrBlock}${defaultMarker}`, value: v.VpcId };
|
|
525
|
+
});
|
|
526
|
+
vpcId = await select({ message: "VPC:", choices });
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
catch {
|
|
531
|
+
console.log(" Could not list VPCs.");
|
|
532
|
+
}
|
|
533
|
+
if (!vpcId) {
|
|
534
|
+
const raw = await input({ message: "Subnet IDs (comma-separated):" });
|
|
535
|
+
return { subnets: raw.split(",").map(s => s.trim()).filter(Boolean), vpcId: "" };
|
|
536
|
+
}
|
|
537
|
+
// List subnets in chosen VPC
|
|
538
|
+
console.log(`\nLooking for subnets in ${vpcId}...`);
|
|
539
|
+
try {
|
|
540
|
+
const data = await ec2Client.send(new DescribeSubnetsCommand({
|
|
541
|
+
Filters: [{ Name: "vpc-id", Values: [vpcId] }],
|
|
542
|
+
}));
|
|
543
|
+
const subnets = data.Subnets || [];
|
|
544
|
+
if (subnets.length > 0) {
|
|
545
|
+
for (const s of subnets) {
|
|
546
|
+
const nameTag = s.Tags?.find((t) => t.Key === "Name")?.Value;
|
|
547
|
+
const label = nameTag ? `${nameTag} (${s.SubnetId})` : s.SubnetId;
|
|
548
|
+
console.log(` ${label} — ${s.AvailabilityZone}, ${s.CidrBlock}`);
|
|
549
|
+
}
|
|
550
|
+
const useAll = await confirm({
|
|
551
|
+
message: `Use all ${subnets.length} subnets? (recommended for multi-AZ)`,
|
|
552
|
+
default: true,
|
|
553
|
+
});
|
|
554
|
+
if (useAll) {
|
|
555
|
+
return { subnets: subnets.map((s) => s.SubnetId), vpcId };
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
console.log(" No subnets found in this VPC.");
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
catch {
|
|
563
|
+
console.log(" Could not list subnets.");
|
|
564
|
+
}
|
|
565
|
+
const raw = await input({ message: "Subnet IDs (comma-separated):" });
|
|
566
|
+
return { subnets: raw.split(",").map(s => s.trim()).filter(Boolean), vpcId };
|
|
567
|
+
}
|
|
568
|
+
async function ensureAppRunnerAccessRole(iamClient) {
|
|
569
|
+
const roleName = AWS_CONSTANTS.APPRUNNER_ACCESS_ROLE;
|
|
570
|
+
console.log(`\nEnsuring App Runner access role (${roleName})...`);
|
|
571
|
+
const trustPolicy = JSON.stringify({
|
|
572
|
+
Version: "2012-10-17",
|
|
573
|
+
Statement: [{
|
|
574
|
+
Effect: "Allow",
|
|
575
|
+
Principal: { Service: "build.apprunner.amazonaws.com" },
|
|
576
|
+
Action: "sts:AssumeRole",
|
|
577
|
+
}],
|
|
578
|
+
});
|
|
579
|
+
try {
|
|
580
|
+
await iamClient.send(new CreateRoleCommand({
|
|
581
|
+
RoleName: roleName,
|
|
582
|
+
AssumeRolePolicyDocument: trustPolicy,
|
|
583
|
+
}));
|
|
584
|
+
console.log(` Created role: ${roleName}`);
|
|
585
|
+
}
|
|
586
|
+
catch (err) {
|
|
587
|
+
if (err.name === "EntityAlreadyExistsException") {
|
|
588
|
+
console.log(` Role already exists`);
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
console.log(` Warning: could not create ${roleName}: ${err.message}`);
|
|
592
|
+
return input({ message: "App Runner access role ARN:" });
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
try {
|
|
596
|
+
await iamClient.send(new AttachRolePolicyCommand({
|
|
597
|
+
RoleName: roleName,
|
|
598
|
+
PolicyArn: "arn:aws:iam::aws:policy/service-role/AWSAppRunnerServicePolicyForECRAccess",
|
|
599
|
+
}));
|
|
600
|
+
console.log(` Attached AWSAppRunnerServicePolicyForECRAccess`);
|
|
601
|
+
}
|
|
602
|
+
catch (err) {
|
|
603
|
+
console.log(` Warning: could not attach ECR access policy: ${err.message}`);
|
|
604
|
+
}
|
|
605
|
+
const data = await iamClient.send(new GetRoleCommand({ RoleName: roleName }));
|
|
606
|
+
return data.Role.Arn;
|
|
607
|
+
}
|
|
608
|
+
async function ensureAppRunnerInstanceRole(iamClient, accountId, region, ecrRepository) {
|
|
609
|
+
const roleName = AWS_CONSTANTS.APPRUNNER_INSTANCE_ROLE;
|
|
610
|
+
console.log(`\nEnsuring App Runner instance role (${roleName})...`);
|
|
611
|
+
const trustPolicy = JSON.stringify({
|
|
612
|
+
Version: "2012-10-17",
|
|
613
|
+
Statement: [{
|
|
614
|
+
Effect: "Allow",
|
|
615
|
+
Principal: { Service: "tasks.apprunner.amazonaws.com" },
|
|
616
|
+
Action: "sts:AssumeRole",
|
|
617
|
+
}],
|
|
618
|
+
});
|
|
619
|
+
try {
|
|
620
|
+
await iamClient.send(new CreateRoleCommand({
|
|
621
|
+
RoleName: roleName,
|
|
622
|
+
AssumeRolePolicyDocument: trustPolicy,
|
|
623
|
+
}));
|
|
624
|
+
console.log(` Created role: ${roleName}`);
|
|
625
|
+
}
|
|
626
|
+
catch (err) {
|
|
627
|
+
if (err.name === "EntityAlreadyExistsException") {
|
|
628
|
+
console.log(` Role already exists`);
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
console.log(` Warning: could not create ${roleName}: ${err.message}`);
|
|
632
|
+
return input({ message: "App Runner instance role ARN:" });
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
const repoName = ecrRepository.split("/").pop();
|
|
636
|
+
const bucketName = AWS_CONSTANTS.buildBucket(accountId, region);
|
|
637
|
+
try {
|
|
638
|
+
await iamClient.send(new PutRolePolicyCommand({
|
|
639
|
+
RoleName: roleName,
|
|
640
|
+
PolicyName: "ActionLlamaScheduler",
|
|
641
|
+
PolicyDocument: JSON.stringify({
|
|
642
|
+
Version: "2012-10-17",
|
|
643
|
+
Statement: [
|
|
644
|
+
{ Sid: "Identity", Effect: "Allow", Action: "sts:GetCallerIdentity", Resource: "*" },
|
|
645
|
+
{ Sid: "ECS", Effect: "Allow", Action: ["ecs:RegisterTaskDefinition", "ecs:RunTask", "ecs:DescribeTasks", "ecs:ListTasks", "ecs:StopTask"], Resource: "*" },
|
|
646
|
+
{ Sid: "Logs", Effect: "Allow", Action: ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", "logs:GetLogEvents", "logs:FilterLogEvents"], Resource: [`arn:aws:logs:${region}:${accountId}:log-group:/ecs/action-llama*`, `arn:aws:logs:${region}:${accountId}:log-group:/aws/lambda/al-*`, `arn:aws:logs:${region}:${accountId}:log-group:/apprunner/al-scheduler*`] },
|
|
647
|
+
{ Sid: "SecretsManager", Effect: "Allow", Action: ["secretsmanager:ListSecrets", "secretsmanager:CreateSecret", "secretsmanager:PutSecretValue", "secretsmanager:GetSecretValue"], Resource: "*" },
|
|
648
|
+
{ Sid: "PassRole", Effect: "Allow", Action: "iam:PassRole", Resource: `arn:aws:iam::${accountId}:role/al-*`, Condition: { StringEquals: { "iam:PassedToService": ["ecs-tasks.amazonaws.com", "codebuild.amazonaws.com", "lambda.amazonaws.com", "apprunner.amazonaws.com"] } } },
|
|
649
|
+
{ Sid: "IAMAgentRoles", Effect: "Allow", Action: ["iam:CreateRole", "iam:GetRole", "iam:GetRolePolicy", "iam:PutRolePolicy", "iam:DeleteRole", "iam:DeleteRolePolicy", "iam:AttachRolePolicy"], Resource: `arn:aws:iam::${accountId}:role/al-*` },
|
|
650
|
+
{ Sid: "IAMListRoles", Effect: "Allow", Action: "iam:ListRoles", Resource: "*" },
|
|
651
|
+
{ Sid: "ECR", Effect: "Allow", Action: ["ecr:BatchGetImage", "ecr:GetDownloadUrlForLayer", "ecr:BatchCheckLayerAvailability", "ecr:GetAuthorizationToken", "ecr:SetRepositoryPolicy"], Resource: "*" },
|
|
652
|
+
{ Sid: "CodeBuild", Effect: "Allow", Action: ["codebuild:StartBuild", "codebuild:BatchGetBuilds", "codebuild:CreateProject"], Resource: `arn:aws:codebuild:${region}:${accountId}:project/al-image-builder` },
|
|
653
|
+
{ Sid: "Lambda", Effect: "Allow", Action: ["lambda:GetFunction", "lambda:CreateFunction", "lambda:UpdateFunctionCode", "lambda:UpdateFunctionConfiguration", "lambda:PutFunctionEventInvokeConfig", "lambda:InvokeFunction"], Resource: `arn:aws:lambda:${region}:${accountId}:function:al-*` },
|
|
654
|
+
{ Sid: "S3", Effect: "Allow", Action: ["s3:CreateBucket", "s3:PutObject", "s3:GetObject", "s3:ListBucket"], Resource: [`arn:aws:s3:::${bucketName}`, `arn:aws:s3:::${bucketName}/*`] },
|
|
655
|
+
{ Sid: "AppRunner", Effect: "Allow", Action: ["apprunner:CreateService", "apprunner:UpdateService", "apprunner:DescribeService", "apprunner:DeleteService"], Resource: `arn:aws:apprunner:${region}:${accountId}:service/al-scheduler/*` },
|
|
656
|
+
{ Sid: "AppRunnerList", Effect: "Allow", Action: "apprunner:ListServices", Resource: "*" },
|
|
657
|
+
],
|
|
658
|
+
}),
|
|
659
|
+
}));
|
|
660
|
+
console.log(` Attached ActionLlamaScheduler policy`);
|
|
661
|
+
}
|
|
662
|
+
catch (err) {
|
|
663
|
+
console.log(` Warning: could not attach policy to ${roleName}: ${err.message}`);
|
|
664
|
+
}
|
|
665
|
+
const data = await iamClient.send(new GetRoleCommand({ RoleName: roleName }));
|
|
666
|
+
return data.Role.Arn;
|
|
667
|
+
}
|
|
668
|
+
async function pickSecurityGroups(ec2Client, vpcId) {
|
|
669
|
+
if (!vpcId) {
|
|
670
|
+
const raw = await input({ message: "Security group IDs (comma-separated, optional):" });
|
|
671
|
+
return raw.trim() ? raw.split(",").map(s => s.trim()).filter(Boolean) : [];
|
|
672
|
+
}
|
|
673
|
+
console.log(`\nLooking for security groups in ${vpcId}...`);
|
|
674
|
+
try {
|
|
675
|
+
const data = await ec2Client.send(new DescribeSecurityGroupsCommand({
|
|
676
|
+
Filters: [{ Name: "vpc-id", Values: [vpcId] }],
|
|
677
|
+
}));
|
|
678
|
+
const sgs = data.SecurityGroups || [];
|
|
679
|
+
if (sgs.length > 0) {
|
|
680
|
+
const choices = [
|
|
681
|
+
...sgs.map((sg) => ({
|
|
682
|
+
name: `${sg.GroupName} (${sg.GroupId})${sg.Description ? ` — ${sg.Description}` : ""}`,
|
|
683
|
+
value: sg.GroupId,
|
|
684
|
+
})),
|
|
685
|
+
{ name: "Skip (use VPC default)", value: "" },
|
|
686
|
+
];
|
|
687
|
+
const choice = await select({ message: "Security group:", choices });
|
|
688
|
+
return choice ? [choice] : [];
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
catch {
|
|
692
|
+
console.log(" Could not list security groups.");
|
|
693
|
+
}
|
|
694
|
+
const raw = await input({ message: "Security group IDs (comma-separated, optional):" });
|
|
695
|
+
return raw.trim() ? raw.split(",").map(s => s.trim()).filter(Boolean) : [];
|
|
696
|
+
}
|
|
697
|
+
//# sourceMappingURL=cloud-setup-ecs.js.map
|