unity-builder/src/model/remote-builder/aws-build-runner.ts

166 lines
5.8 KiB
TypeScript

import * as AWS from 'aws-sdk';
import RemoteBuilderEnvironmentVariable from './remote-builder-environment-variable';
import * as core from '@actions/core';
import RemoteBuilderTaskDef from './remote-builder-task-def';
import * as zlib from 'zlib';
class AWSBuildRunner {
static async runTask(
taskDef: RemoteBuilderTaskDef,
ECS: AWS.ECS,
CF: AWS.CloudFormation,
environment: RemoteBuilderEnvironmentVariable[],
buildUid: string,
) {
const cluster = taskDef.baseResources?.find((x) => x.LogicalResourceId === 'ECSCluster')?.PhysicalResourceId || '';
const taskDefinition =
taskDef.taskDefResources?.find((x) => x.LogicalResourceId === 'TaskDefinition')?.PhysicalResourceId || '';
const SubnetOne =
taskDef.baseResources?.find((x) => x.LogicalResourceId === 'PublicSubnetOne')?.PhysicalResourceId || '';
const SubnetTwo =
taskDef.baseResources?.find((x) => x.LogicalResourceId === 'PublicSubnetTwo')?.PhysicalResourceId || '';
const ContainerSecurityGroup =
taskDef.baseResources?.find((x) => x.LogicalResourceId === 'ContainerSecurityGroup')?.PhysicalResourceId || '';
const streamName =
taskDef.taskDefResources?.find((x) => x.LogicalResourceId === 'KinesisStream')?.PhysicalResourceId || '';
const task = await ECS.runTask({
cluster,
taskDefinition,
platformVersion: '1.4.0',
overrides: {
containerOverrides: [
{
name: taskDef.taskDefStackName,
environment: [...environment, { name: 'BUILDID', value: buildUid }],
},
],
},
launchType: 'FARGATE',
networkConfiguration: {
awsvpcConfiguration: {
subnets: [SubnetOne, SubnetTwo],
assignPublicIp: 'ENABLED',
securityGroups: [ContainerSecurityGroup],
},
},
}).promise();
core.info('Task is starting on worker cluster');
const taskArn = task.tasks?.[0].taskArn || '';
try {
await ECS.waitFor('tasksRunning', { tasks: [taskArn], cluster }).promise();
} catch (error) {
await new Promise((resolve) => setTimeout(resolve, 3000));
const describeTasks = await ECS.describeTasks({
tasks: [taskArn],
cluster,
}).promise();
core.info(`Task has ended ${describeTasks.tasks?.[0].containers?.[0].lastStatus}`);
core.setFailed(error);
core.error(error);
}
core.info(`Task is running on worker cluster`);
await this.streamLogsUntilTaskStops(ECS, CF, taskDef, cluster, taskArn, streamName);
await ECS.waitFor('tasksStopped', { cluster, tasks: [taskArn] }).promise();
const exitCode = (
await ECS.describeTasks({
tasks: [taskArn],
cluster,
}).promise()
).tasks?.[0].containers?.[0].exitCode;
if (exitCode !== 0) {
core.error(`job failed with exit code ${exitCode}`);
throw new Error(`job failed with exit code ${exitCode}`);
} else {
core.info(`Task has finished successfully`);
}
}
static async streamLogsUntilTaskStops(
ECS: AWS.ECS,
CF: AWS.CloudFormation,
taskDef: RemoteBuilderTaskDef,
clusterName: string,
taskArn: string,
kinesisStreamName: string,
) {
// watching logs
const kinesis = new AWS.Kinesis();
const getTaskData = async () => {
const tasks = await ECS.describeTasks({
cluster: clusterName,
tasks: [taskArn],
}).promise();
return tasks.tasks?.[0];
};
const stream = await kinesis
.describeStream({
StreamName: kinesisStreamName,
})
.promise();
let iterator =
(
await kinesis
.getShardIterator({
ShardIteratorType: 'TRIM_HORIZON',
StreamName: stream.StreamDescription.StreamName,
ShardId: stream.StreamDescription.Shards[0].ShardId,
})
.promise()
).ShardIterator || '';
await CF.waitFor('stackCreateComplete', { StackName: taskDef.taskDefStackNameTTL }).promise();
core.info(`Task status is ${(await getTaskData())?.lastStatus}`);
const logBaseUrl = `https://${AWS.config.region}.console.aws.amazon.com/cloudwatch/home?region=${AWS.config.region}#logsV2:log-groups/log-group/${taskDef.taskDefStackName}`;
core.info(`You can also see the logs at AWS Cloud Watch: ${logBaseUrl}`);
let readingLogs = true;
let timestamp: number = 0;
while (readingLogs) {
await new Promise((resolve) => setTimeout(resolve, 1500));
const taskData = await getTaskData();
if (taskData?.lastStatus !== 'RUNNING') {
if (timestamp === 0) {
core.info('Task stopped, streaming end of logs');
timestamp = Date.now();
}
if (timestamp !== 0 && Date.now() - timestamp < 30000) {
core.info('Task status is not RUNNING for 30 seconds, last query for logs');
readingLogs = false;
}
}
const records = await kinesis
.getRecords({
ShardIterator: iterator,
})
.promise();
iterator = records.NextShardIterator || '';
if (records.Records.length > 0 && iterator) {
for (let index = 0; index < records.Records.length; index++) {
const json = JSON.parse(
zlib.gunzipSync(Buffer.from(records.Records[index].Data as string, 'base64')).toString('utf8'),
);
if (json.messageType === 'DATA_MESSAGE') {
for (let logEventsIndex = 0; logEventsIndex < json.logEvents.length; logEventsIndex++) {
if (json.logEvents[logEventsIndex].message.includes(taskDef.logid)) {
core.info('End of task logs');
readingLogs = false;
} else {
core.info(json.logEvents[logEventsIndex].message);
}
}
}
}
}
}
}
}
export default AWSBuildRunner;