Skip to content

Commit

Permalink
Merge pull request #47 from Ayush272002/feat/engine
Browse files Browse the repository at this point in the history
add basic logic to pull from queue and ssh into an ec2 instance
  • Loading branch information
highjeans authored Dec 10, 2024
2 parents 3d99f67 + 61a32be commit c67ede2
Show file tree
Hide file tree
Showing 23 changed files with 19,347 additions and 17,651 deletions.
53 changes: 0 additions & 53 deletions MarkingScripts/mark.sh

This file was deleted.

5 changes: 4 additions & 1 deletion apps/backend/.env.example
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
PORT=
PORT=8000
JWT_SECRET=secret
ALLOWED_ORIGINS=http://localhost:3000,ksgdkq
REDIS_URL=
23 changes: 18 additions & 5 deletions apps/backend/src/controllers/assignmentController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ const getAssignment = async (req: Request, res: Response) => {
createdAt: true,
updatedAt: true,
maxMarks: true,
boilerplate: true,
},
where: {
id: parsedData.data.assignmentId,
Expand Down Expand Up @@ -151,6 +152,7 @@ const getAssignment = async (req: Request, res: Response) => {
});
}

console.log(assignmentDescription);
return res.status(200).json({ ...assignmentDescription });
};

Expand Down Expand Up @@ -240,7 +242,7 @@ const submitAssignment = async (req: Request, res: Response) => {
},
select: {
markingScript: true,
requiredFiles: true,
dockerFile: true,
},
},
},
Expand All @@ -260,9 +262,10 @@ const submitAssignment = async (req: Request, res: Response) => {
const assignmentZip = parsedData.data.assignmentZip;
const payload = {
markingScript: user.student.courses[0].assignments[0].markingScript,
requiredFiles: user.student.courses[0].assignments[0].requiredFiles,
studentId: parsedData.data.userId,
dockerFile: user.student.courses[0].assignments[0].dockerFile,
userId: parsedData.data.userId,
assignmentId: parsedData.data.assignmentId,
uploadLink: parsedData.data.assignmentZip,
};

await prisma.submission.create({
Expand Down Expand Up @@ -312,8 +315,16 @@ const submitAssignment = async (req: Request, res: Response) => {
};

const createAssignment = async (req: Request, res: Response) => {
const { title, description, dueDate, maxMarks, courseId, markingScript } =
req.body;
const {
title,
description,
dueDate,
maxMarks,
courseId,
markingScript,
dockerFile,
boilerplate,
} = req.body;

if (!title || !dueDate || !maxMarks || !courseId) {
return res.status(400).json({
Expand All @@ -340,6 +351,8 @@ const createAssignment = async (req: Request, res: Response) => {
maxMarks: parseInt(maxMarks, 10),
courseId,
markingScript,
dockerFile,
boilerplate,
},
});

Expand Down
3 changes: 3 additions & 0 deletions apps/engine/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
PRIVATE_KEY=
HOST=
USERNAME=ubuntu
1 change: 1 addition & 0 deletions apps/engine/esbuild.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const esbuild = require('esbuild');
esbuild
.build({
entryPoints: ['./src/**/*.ts'],
loader: { '.node': 'file' },
bundle: true,
platform: 'node',
outdir: 'dist',
Expand Down
6 changes: 4 additions & 2 deletions apps/engine/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
"description": "",
"dependencies": {
"@repo/kafka": "*",
"dotenv": "^16.4.5",
"esbuild": "^0.24.0"
"axios": "^1.7.9",
"dotenv": "^16.4.7",
"esbuild": "^0.24.0",
"node-ssh": "^13.2.0"
}
}
16 changes: 14 additions & 2 deletions apps/engine/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
import kafkaClient from '@repo/kafka/client';
import { SUBMIT } from '@repo/topics/topics';
import dotenv from 'dotenv';
import { processSubmission } from './utils/submission';
import { sshIntoEC2 } from './utils/ssh';

dotenv.config();

const topic = SUBMIT;

async function consumeMessages() {
try {
const kafka = kafkaClient.getInstance();
const consumer = kafka.consumer({ groupId: 'your-consumer-group-id' });
const consumer = kafka.consumer({ groupId: 'assignment-submission' });
await consumer.connect();
await consumer.subscribe({ topic, fromBeginning: true });
await consumer.subscribe({ topic, fromBeginning: false });

await consumer.run({
// @ts-ignore
eachMessage: async ({ topic, partition, message }) => {
const messageValue = message.value?.toString() || '';
console.log(`Received message: ${messageValue}`);
try {
const submission = JSON.parse(messageValue);
await processSubmission(submission);
} catch (err) {
console.log('Error processing message:', err);
}
},
});

Expand All @@ -25,3 +36,4 @@ async function consumeMessages() {
}

consumeMessages();
// sshIntoEC2();
157 changes: 157 additions & 0 deletions apps/engine/src/utils/submission.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { NodeSSH } from 'node-ssh';
import fs from 'fs';
import path from 'path';
import prisma from '@repo/db/client';

async function convertLineEndings(ssh: NodeSSH, filePath: string) {
const result = await ssh.execCommand(`
if command -v dos2unix >/dev/null 2>&1; then
dos2unix ${filePath}
else
sed -i 's/\r$//' ${filePath}
fi
`);

if (result.code !== 0) {
console.error('Failed to convert line endings:', result.stderr);
throw new Error('Failed to convert line endings');
}
}

export async function processSubmission(submission: any) {
const { assignmentId, uploadLink, markingScript, dockerFile } = submission;

try {
console.log('Setting up SSH connection...');
const ssh = new NodeSSH();

const host = process.env.HOST;
const username = process.env.AWS_USERNAME;
const privateKey = fs.readFileSync(
process.env.PRIVATE_KEY as string,
'utf-8'
);

await ssh.connect({ host, username, privateKey });
console.log('Connected to EC2 instance.');

const remoteWorkDir = `/tmp/${assignmentId}_work`;

console.log('Creating working directory on EC2...');
await ssh.execCommand(`mkdir -p ${remoteWorkDir}`);

// Extract original file extension from upload link
const fileExtension = path.extname(uploadLink);
const submissionFileName = `solution${fileExtension}`;

// Download submission with original extension
console.log('Downloading submission on EC2...');
const downloadSubmission = `curl -o ${remoteWorkDir}/${submissionFileName} '${uploadLink}'`;
await ssh.execCommand(downloadSubmission);

const validateSubmission = `test -f ${remoteWorkDir}/${submissionFileName} && echo "File exists" || echo "File missing"`;
const submissionCheck = await ssh.execCommand(validateSubmission);
if (!submissionCheck.stdout.includes('File exists')) {
throw new Error(`Failed to download ${submissionFileName}`);
}
console.log(`Submission file downloaded as ${submissionFileName}`);

// Download and validate mark.sh
console.log('Downloading marking script...');
const downloadMarkingScript = `curl -o ${remoteWorkDir}/mark.sh '${markingScript}'`;
await ssh.execCommand(downloadMarkingScript);
await ssh.execCommand(`chmod +x ${remoteWorkDir}/mark.sh`);

console.log('Converting line endings...');
await convertLineEndings(ssh, `${remoteWorkDir}/mark.sh`);

const validateMarkingScript = `test -f ${remoteWorkDir}/mark.sh && echo "File exists" || echo "File missing"`;
const markingScriptCheck = await ssh.execCommand(validateMarkingScript);
if (!markingScriptCheck.stdout.includes('File exists')) {
throw new Error('Failed to download mark.sh');
}
console.log(
'Marking script downloaded, made executable, and line endings converted.'
);

// Download and validate Dockerfile
console.log('Downloading Dockerfile...');
const downloadDockerFile = `curl -o ${remoteWorkDir}/Dockerfile '${dockerFile}'`;
await ssh.execCommand(downloadDockerFile);

const validateDockerFile = `test -f ${remoteWorkDir}/Dockerfile && echo "File exists" || echo "File missing"`;
const dockerFileCheck = await ssh.execCommand(validateDockerFile);
if (!dockerFileCheck.stdout.includes('File exists')) {
throw new Error('Failed to download Dockerfile');
}
console.log('Dockerfile downloaded.');

// Build Docker image
console.log('Building Docker image...');
const imageName = `assignment_${assignmentId}:latest`;
const buildCommand = `cd ${remoteWorkDir} && sudo docker build -t ${imageName} .`;
const buildResult = await ssh.execCommand(buildCommand);

if (buildResult.code !== 0) {
console.error('Build errors:', buildResult.stderr);
throw new Error(
'Docker build failed. Check the Dockerfile or input files.'
);
}
console.log(`Docker image ${imageName} built successfully.`);
console.log('Build output:', buildResult.stdout, buildResult.stderr);

// Run Docker container
console.log('Running Docker container...');
const containerName = `assignment_${assignmentId}_container`;
const runCommand = `sudo docker run --name ${containerName} ${imageName}`;
const runResult = await ssh.execCommand(runCommand);
console.log('Container output:', runResult.stdout);
if (runResult.stderr) console.error('Container errors:', runResult.stderr);

// Process results
console.log('Validating test results from container output...');
const testsOutput = runResult.stdout || '';
const passedTestsMatch = testsOutput.match(/Number of tests passed: (\d+)/);
const passedTests = passedTestsMatch ? parseInt(passedTestsMatch[1]!) : 0;
const totalTests = 4;
const marks = Math.round((passedTests / totalTests) * 100);

console.log(`Tests passed: ${passedTests}/${totalTests}`);
console.log(`Marks calculated: ${marks}`);

const submissionRecord = await prisma.submission.findFirst({
where: { assignmentId },
select: { id: true },
});

if (!submissionRecord) {
console.error(`Submission not found for assignmentId: ${assignmentId}`);
return;
}

await prisma.submission.update({
where: { id: submissionRecord.id },
data: {
marksAchieved: marks,
logs: testsOutput,
},
});
console.log('Marks updated in the database.');

console.log('Cleaning up EC2 resources...');
await ssh.execCommand(
`sudo docker stop ${containerName} && sudo docker rm ${containerName}`
);
await ssh.execCommand(`sudo docker rmi ${imageName}`);
await ssh.execCommand(`rm -rf ${remoteWorkDir}`);

ssh.dispose();
console.log('Process complete.');
} catch (error) {
console.error('Error processing submission:', error);
throw error;
}
}

// sudo apt-get update && sudo apt-get install -y dos2unix
Loading

0 comments on commit c67ede2

Please sign in to comment.