diff --git a/bin/apply-config.mjs b/bin/apply-config.mjs new file mode 100755 index 0000000..87dafae --- /dev/null +++ b/bin/apply-config.mjs @@ -0,0 +1,99 @@ +import { LambdaClient, InvokeCommand } from "@aws-sdk/client-lambda"; +import { + ECSClient, + DescribeTasksCommand, + DescribeTaskDefinitionCommand, +} from "@aws-sdk/client-ecs"; +import { + CloudWatchLogsClient, + GetLogEventsCommand, +} from "@aws-sdk/client-cloudwatch-logs"; + +let exitCode = null; + +main() + .then((logs) => { + console.log("Task output:"); + logs.forEach((l) => console.log(l)); + }) + .catch((error) => { + console.error(`Error: ${error}`); + exitCode = 1; + }) + .finally(() => process.exit(exitCode)); + +async function main() { + const [, , lambdaArn] = process.argv; + const { taskArn, clusterArn } = await invokeLambda(lambdaArn); + const ecs = new ECSClient({}); + const task = await pollEcsTask(ecs, taskArn, clusterArn); + exitCode = task.containers[0]?.exitCode ?? null; + if (exitCode === null) + throw new Error("Could not retrieve exit code from the ECS task."); + + const { logGroup, logStreamPrefix, region, containerName } = + await getLogConfig(ecs, task.taskDefinitionArn); + const taskId = taskArn.split("/").pop(); + const logStreamName = `${logStreamPrefix}/${containerName}/${taskId}`; + return fetchCloudWatchLogs(logGroup, logStreamName, region); +} + +async function invokeLambda(lambdaArn) { + const lambda = new LambdaClient({}); + const response = await lambda.send( + new InvokeCommand({ + FunctionName: lambdaArn, + InvocationType: "RequestResponse", + Payload: new TextEncoder().encode(JSON.stringify({})), + }) + ); + return JSON.parse(new TextDecoder().decode(response.Payload)); +} + +async function pollEcsTask(ecs, taskArn, clusterArn) { + while (true) { + const { tasks } = await ecs.send( + new DescribeTasksCommand({ cluster: clusterArn, tasks: [taskArn] }) + ); + const task = tasks?.[0]; + if (!task) throw new Error(`No tasks found with taskArn: ${taskArn}`); + console.log(`Task status: ${task.lastStatus}`); + if (task.lastStatus === "STOPPED") return task; + await new Promise((resolve) => setTimeout(resolve, 5000)); + } +} + +async function getLogConfig(ecs, taskDefinitionArn) { + const { taskDefinition } = await ecs.send( + new DescribeTaskDefinitionCommand({ taskDefinition: taskDefinitionArn }) + ); + const containerDef = taskDefinition.containerDefinitions[0]; + if (containerDef.logConfiguration?.logDriver !== "awslogs") + throw new Error("Log driver is not 'awslogs'."); + return { + logGroup: containerDef.logConfiguration.options["awslogs-group"], + logStreamPrefix: + containerDef.logConfiguration.options["awslogs-stream-prefix"], + region: containerDef.logConfiguration.options["awslogs-region"], + containerName: containerDef.name, + }; +} + +async function fetchCloudWatchLogs(logGroup, logStreamName, region) { + const logsClient = new CloudWatchLogsClient({ region }); + let nextToken, + events = []; + do { + const { events: newEvents, nextForwardToken } = await logsClient.send( + new GetLogEventsCommand({ + logGroupName: logGroup, + logStreamName, + startFromHead: true, + nextToken, + }) + ); + events.push(...newEvents); + nextToken = nextForwardToken !== nextToken ? nextForwardToken : null; + } while (nextToken); + return events.map((event) => event.message); +} diff --git a/config/apply-config.mjs b/config/apply-config.mjs deleted file mode 100755 index 30d89d3..0000000 --- a/config/apply-config.mjs +++ /dev/null @@ -1,146 +0,0 @@ -import { LambdaClient, InvokeCommand } from "@aws-sdk/client-lambda"; -import { - ECSClient, - DescribeTasksCommand, - DescribeTaskDefinitionCommand, -} from "@aws-sdk/client-ecs"; -import { - CloudWatchLogsClient, - GetLogEventsCommand, -} from "@aws-sdk/client-cloudwatch-logs"; - -async function main() { - const [_, __, lambdaArn] = process.argv; - - // Set up AWS clients - const lambda = new LambdaClient({}); - const ecs = new ECSClient({}); - const logs = new CloudWatchLogsClient({}); - - // Step 1: Invoke the Lambda function - let taskArn, clusterArn; - try { - const response = await lambda.send( - new InvokeCommand({ - FunctionName: lambdaArn, - InvocationType: "RequestResponse", - Payload: new TextEncoder().encode(JSON.stringify({})), - }) - ); - - const payload = JSON.parse(new TextDecoder().decode(response.Payload)); - taskArn = payload.taskArn; - clusterArn = payload.clusterArn; - console.log( - `Invoked Lambda function. Received taskArn=${taskArn}, clusterArn=${clusterArn}` - ); - } catch (error) { - console.error(`Error invoking Lambda function: ${error}`); - process.exit(1); - } - - // Step 2: Poll the ECS task until it reaches 'STOPPED' status - let task; - try { - while (true) { - const response = await ecs.send( - new DescribeTasksCommand({ - cluster: clusterArn, - tasks: [taskArn], - }) - ); - - if (!response.tasks || response.tasks.length === 0) { - console.error(`No tasks found with taskArn: ${taskArn}`); - process.exit(1); - } - - task = response.tasks[0]; - const lastStatus = task.lastStatus; - console.log(`Task status: ${lastStatus}`); - - if (lastStatus === "STOPPED") break; - await new Promise((resolve) => setTimeout(resolve, 5000)); - } - } catch (error) { - console.error(`Error polling ECS task: ${error}`); - process.exit(1); - } - - // Retrieve the exit code from the task's containers - const exitCode = task.containers.find( - (container) => "exitCode" in container - )?.exitCode; - - if (exitCode === undefined) { - console.error("Could not retrieve exit code from the ECS task."); - process.exit(1); - } - - // Step 3: Retrieve log configuration from the task definition - let logGroup, logStreamName, region; - try { - const taskDefinitionArn = task.taskDefinitionArn; - const response = await ecs.send( - new DescribeTaskDefinitionCommand({ - taskDefinition: taskDefinitionArn, - }) - ); - - const containerDefinition = response.taskDefinition.containerDefinitions[0]; - const logConfiguration = containerDefinition.logConfiguration || {}; - - if (logConfiguration.logDriver !== "awslogs") { - console.error("Log driver is not 'awslogs'."); - process.exit(1); - } - - const options = logConfiguration.options || {}; - logGroup = options["awslogs-group"]; - const logStreamPrefix = options["awslogs-stream-prefix"]; - region = options["awslogs-region"]; - - const containerName = containerDefinition.name; - const taskId = taskArn.split("/").pop(); - logStreamName = `${logStreamPrefix}/${containerName}/${taskId}`; - } catch (error) { - console.error(`Error retrieving log configuration: ${error}`); - process.exit(1); - } - - // Step 4: Retrieve and print the CloudWatch logs - try { - const logsClient = new CloudWatchLogsClient({ region }); - let nextToken; - const events = []; - - while (true) { - const params = { - logGroupName: logGroup, - logStreamName: logStreamName, - startFromHead: true, - ...(nextToken && { nextToken }), - }; - - const response = await logsClient.send(new GetLogEventsCommand(params)); - events.push(...response.events); - - if (!response.nextForwardToken || response.nextForwardToken === nextToken) - break; - nextToken = response.nextForwardToken; - } - - events.forEach((event) => console.log(event.message)); - } catch (error) { - console.error(`Error retrieving CloudWatch logs: ${error}`); - process.exit(1); - } - - // Exit with the exit code from the ECS task - process.exit(exitCode); -} - -main().catch((error) => { - console.error(error); - process.exit(1); -}); diff --git a/package.json b/package.json index b340c50..d0b2d08 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "build": "tsc", "watch": "tsc -w", "cdk": "cdk", - "apply-config": "node ./config/apply-config.mjs" + "apply-config": "node ./bin/apply-config.mjs" }, "devDependencies": { "@aws-sdk/client-cloudwatch-logs": "^3.680.0",