diff --git a/orchestrator/init_dev.sh b/orchestrator/init_dev.sh index c235bf88..3091adda 100755 --- a/orchestrator/init_dev.sh +++ b/orchestrator/init_dev.sh @@ -1,7 +1,7 @@ #!/bin/bash prisma_migration () { docker pull postgres:10 - docker run -p 5432:5432 -v db-data:/var/lib/postgresql/data -e POSTGRES_PASSWORD=password \ + docker run -p 5434:5432 -v db-data:/var/lib/postgresql/data -e POSTGRES_PASSWORD=password \ -d --name postgres postgres:10 npx prisma migrate dev --name init } diff --git a/orchestrator/packages/api/src/__mocks__/grader-image-build-request.ts b/orchestrator/packages/api/src/__mocks__/grader-image-build-request.ts index bd2ce112..f8e78bea 100644 --- a/orchestrator/packages/api/src/__mocks__/grader-image-build-request.ts +++ b/orchestrator/packages/api/src/__mocks__/grader-image-build-request.ts @@ -1,6 +1,8 @@ import { GraderImageBuildRequest } from "@codegrade-orca/common"; export const defaultGraderImageBuildRequest: GraderImageBuildRequest = { - dockerfileContents: `FROM hello-world:latest`, - dockerfileSHASum: "generated-sha-sum", + dockerfile_contents: `FROM hello-world:latest`, + dockerfile_sha_sum: "generated-sha-sum", + response_url: "http://example.com/response", + build_key: "{\"grader_id\": 1}" }; diff --git a/orchestrator/packages/api/src/controllers/grading-queue-controller.ts b/orchestrator/packages/api/src/controllers/grading-queue-controller.ts index c3b949e0..4bc86ad4 100644 --- a/orchestrator/packages/api/src/controllers/grading-queue-controller.ts +++ b/orchestrator/packages/api/src/controllers/grading-queue-controller.ts @@ -5,9 +5,10 @@ import { getPageFromGradingQueue as getPageFromGradingJobs, } from "../utils/pagination"; import { validateFilterRequest } from "../utils/validate"; -import { errorResponse } from "./utils"; +import { errorResponse, notifyClientOfCancelledJob } from "./utils"; import { GradingJob, + GradingJobConfig, filterGradingJobs, getFilterInfo, getGradingQueueStats, @@ -163,7 +164,9 @@ export const deleteJob = async (req: Request, res: Response) => { if (isNaN(jobID)) { return errorResponse(res, 400, ["Given job ID is not a number."]); } - await deleteJobInQueue(jobID); + const deletedJob = await deleteJobInQueue(jobID); + const deletedJobConfig = deletedJob.config as object as GradingJobConfig; + await notifyClientOfCancelledJob(deletedJobConfig) return res.status(200).json({ message: "OK" }); } catch (err) { if (err instanceof GradingQueueOperationException) { diff --git a/orchestrator/packages/api/src/controllers/utils.ts b/orchestrator/packages/api/src/controllers/utils.ts index 29dee054..fcf75b3a 100644 --- a/orchestrator/packages/api/src/controllers/utils.ts +++ b/orchestrator/packages/api/src/controllers/utils.ts @@ -1,3 +1,4 @@ +import { GradingJobConfig, GradingJobResult } from "@codegrade-orca/common"; import { Response } from "express"; export const errorResponse = ( @@ -7,3 +8,21 @@ export const errorResponse = ( ) => { return res.status(status).json({ errors: errors }); }; + +export const notifyClientOfCancelledJob = async (jobConfig: GradingJobConfig) => { + const result: GradingJobResult = { + shell_responses: [], + errors: ["Job cancelled by a course professor or Orca admin."] + }; + await fetch(jobConfig.response_url, { + method: "POST", + headers: { + "Accept": "application/json", + "Content-Type": "application/json" + }, + body: JSON.stringify({ result, key: jobConfig.key }) + }).catch((err) => + console.error( + `Encountered the following error while attempting to notify client of Job cancellation: ${err}` + )); +} diff --git a/orchestrator/packages/common/src/types/image-build-service.ts b/orchestrator/packages/common/src/types/image-build-service.ts index 279b4c12..8cc78b67 100644 --- a/orchestrator/packages/common/src/types/image-build-service.ts +++ b/orchestrator/packages/common/src/types/image-build-service.ts @@ -1,4 +1,27 @@ +export interface GraderImageBuildResult { + was_successful: boolean, + logs: Array +} + +export type ImageBuildStep = "Write request contents to Dockerfile." | + "Run docker build on Dockerfile." | + "Save image to .tgz file." | + "Remove Dockerfile."; + +export interface ImageBuildLog { + step: ImageBuildStep, + output?: string, + error?: string +} + +export const isImageBuildResult = (o: unknown): o is GraderImageBuildResult => { + if (typeof o !== "object" || o === null) return false; + return Object.keys(o).length == 2 && ["was_successful", "logs"].every((k) => k in o); +} + export interface GraderImageBuildRequest { - dockerfileContents: string; - dockerfileSHASum: string; + dockerfile_contents: string, + dockerfile_sha_sum: string, + response_url: string, + build_key: string, } diff --git a/orchestrator/packages/common/src/types/index.ts b/orchestrator/packages/common/src/types/index.ts index 23b33737..4b97bf8b 100644 --- a/orchestrator/packages/common/src/types/index.ts +++ b/orchestrator/packages/common/src/types/index.ts @@ -1,2 +1,3 @@ export * from "./grading-queue"; export * from "./image-build-service"; +export * from "./job-result"; diff --git a/orchestrator/packages/common/src/types/job-result.ts b/orchestrator/packages/common/src/types/job-result.ts new file mode 100644 index 00000000..a93fbf45 --- /dev/null +++ b/orchestrator/packages/common/src/types/job-result.ts @@ -0,0 +1,14 @@ +export interface GradingJobResult { + shell_responses: Array, + errors: Array, + output?: string +} + +interface GradingScriptCommandResponse { + cmd: string | Array, + stdout: string, + stderr: string, + did_timeout: boolean, + is_error: boolean, + status_code: number +} diff --git a/orchestrator/packages/common/src/validations/__mocks__/grader-image-build-request.ts b/orchestrator/packages/common/src/validations/__mocks__/grader-image-build-request.ts index a1b0ae15..84176eb9 100644 --- a/orchestrator/packages/common/src/validations/__mocks__/grader-image-build-request.ts +++ b/orchestrator/packages/common/src/validations/__mocks__/grader-image-build-request.ts @@ -1,6 +1,8 @@ import { GraderImageBuildRequest } from "../../types/image-build-service"; export const defaultGraderImageBuildRequest: GraderImageBuildRequest = { - dockerfileContents: `FROM hello-world:latest`, - dockerfileSHASum: "generated-sha-sum", + dockerfile_contents: `FROM hello-world:latest`, + dockerfile_sha_sum: "generated-sha-sum", + response_url: "http://example.com/response", + build_key: "{\"grader_id\": 1}" }; diff --git a/orchestrator/packages/common/src/validations/schemas/grader-image-build-request.ts b/orchestrator/packages/common/src/validations/schemas/grader-image-build-request.ts index d9e75684..f033fb80 100644 --- a/orchestrator/packages/common/src/validations/schemas/grader-image-build-request.ts +++ b/orchestrator/packages/common/src/validations/schemas/grader-image-build-request.ts @@ -2,7 +2,9 @@ export const graderImageBuildRequestSchema = { $id: "https://orca-schemas.com/grader-image-build-request", type: "object", properties: { - dockerfileContents: { type: "string" }, - dockerfileSHASum: { type: "string" }, + dockerfile_contents: { type: "string" }, + dockerfile_sha_sum: { type: "string" }, + response_url: { type: "string" }, + build_key: { type: "string" }, }, } as const; diff --git a/orchestrator/packages/db/prisma/migrations/20240327003906_init/migration.sql b/orchestrator/packages/db/prisma/migrations/20240720201049_init/migration.sql similarity index 93% rename from orchestrator/packages/db/prisma/migrations/20240327003906_init/migration.sql rename to orchestrator/packages/db/prisma/migrations/20240720201049_init/migration.sql index 89510678..f11a1e9e 100644 --- a/orchestrator/packages/db/prisma/migrations/20240327003906_init/migration.sql +++ b/orchestrator/packages/db/prisma/migrations/20240720201049_init/migration.sql @@ -38,6 +38,8 @@ CREATE TABLE "Job" ( CREATE TABLE "ImageBuildInfo" ( "dockerfileSHA" TEXT NOT NULL, "dockerfileContent" TEXT NOT NULL, + "buildKey" TEXT NOT NULL, + "responseURL" TEXT NOT NULL, "inProgress" BOOLEAN NOT NULL DEFAULT false, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, @@ -65,6 +67,9 @@ CREATE UNIQUE INDEX "Submitter_clientURL_collationType_collationID_key" ON "Subm -- CreateIndex CREATE UNIQUE INDEX "Job_clientURL_clientKey_key" ON "Job"("clientURL", "clientKey"); +-- CreateIndex +CREATE UNIQUE INDEX "ImageBuildInfo_buildKey_responseURL_key" ON "ImageBuildInfo"("buildKey", "responseURL"); + -- CreateIndex CREATE UNIQUE INDEX "JobConfigAwaitingImage_clientKey_clientURL_key" ON "JobConfigAwaitingImage"("clientKey", "clientURL"); diff --git a/orchestrator/packages/db/prisma/schema.prisma b/orchestrator/packages/db/prisma/schema.prisma index 8beeb329..64ddf051 100644 --- a/orchestrator/packages/db/prisma/schema.prisma +++ b/orchestrator/packages/db/prisma/schema.prisma @@ -60,10 +60,14 @@ model Job { model ImageBuildInfo { dockerfileSHA String @id dockerfileContent String + buildKey String + responseURL String inProgress Boolean @default(false) createdAt DateTime @default(now()) jobConfigs JobConfigAwaitingImage[] + + @@unique([buildKey, responseURL]) } model JobConfigAwaitingImage { diff --git a/orchestrator/packages/db/run-migration b/orchestrator/packages/db/run-migration index 1bbf15a8..fa8b8e76 100755 --- a/orchestrator/packages/db/run-migration +++ b/orchestrator/packages/db/run-migration @@ -1,5 +1,7 @@ #!/bin/bash +set -e + if [[ "$#" -ne 1 ]]; then echo "Please pass a *single* argument for migration name." exit 1 diff --git a/orchestrator/packages/db/src/image-builder-operations/index.ts b/orchestrator/packages/db/src/image-builder-operations/index.ts index 22d79d0e..35bac9ff 100644 --- a/orchestrator/packages/db/src/image-builder-operations/index.ts +++ b/orchestrator/packages/db/src/image-builder-operations/index.ts @@ -2,4 +2,4 @@ import getNextImageBuild from "./get-next-image-build"; import handleCompletedImageBuild from "./handle-completed-image-build"; export { getNextImageBuild }; -export { handleCompletedImageBuild }; +export { handleCompletedImageBuild }; diff --git a/orchestrator/packages/db/src/server-operations/delete-job.ts b/orchestrator/packages/db/src/server-operations/delete-job.ts index 9ba8b2a3..7c2be4fe 100644 --- a/orchestrator/packages/db/src/server-operations/delete-job.ts +++ b/orchestrator/packages/db/src/server-operations/delete-job.ts @@ -2,7 +2,7 @@ import { getAssociatedReservation, retireReservationAndJob } from "../utils"; import prismaInstance from "../prisma-instance"; import { Job } from "@prisma/client"; -const deleteJob = (jobID: number) => +const deleteJob = (jobID: number): Promise => prismaInstance.$transaction(async (tx) => { const job = await tx.job.findUnique({ where: { @@ -11,6 +11,7 @@ const deleteJob = (jobID: number) => }) as Job; const reservation = await getAssociatedReservation(job, tx); await retireReservationAndJob(job, reservation, tx); + return job; }); export default deleteJob; diff --git a/orchestrator/packages/db/src/server-operations/enqueue-image-build.ts b/orchestrator/packages/db/src/server-operations/enqueue-image-build.ts index 4522c482..fdd57b15 100644 --- a/orchestrator/packages/db/src/server-operations/enqueue-image-build.ts +++ b/orchestrator/packages/db/src/server-operations/enqueue-image-build.ts @@ -1,12 +1,11 @@ import { GraderImageBuildRequest } from '@codegrade-orca/common'; import prismaInstance from '../prisma-instance'; -const enqueueImageBuild = ({ dockerfileSHASum, dockerfileContents }: GraderImageBuildRequest): Promise => { +const enqueueImageBuild = ({ dockerfile_sha_sum, dockerfile_contents, response_url, build_key }: GraderImageBuildRequest): Promise => { return prismaInstance.$transaction(async (tx) => { const buildInfoAlreadyExists = (await tx.imageBuildInfo.count({ where: { - dockerfileSHA: dockerfileSHASum, - dockerfileContent: dockerfileContents + dockerfileSHA: dockerfile_sha_sum, } })) > 0; if (buildInfoAlreadyExists) { @@ -14,8 +13,10 @@ const enqueueImageBuild = ({ dockerfileSHASum, dockerfileContents }: GraderImage } else { await tx.imageBuildInfo.create({ data: { - dockerfileSHA: dockerfileSHASum, - dockerfileContent: dockerfileContents + dockerfileSHA: dockerfile_sha_sum, + dockerfileContent: dockerfile_contents, + responseURL: response_url, + buildKey: build_key } }); return true; diff --git a/orchestrator/packages/image-build-service/src/index.ts b/orchestrator/packages/image-build-service/src/index.ts index fdda2611..eeb44997 100644 --- a/orchestrator/packages/image-build-service/src/index.ts +++ b/orchestrator/packages/image-build-service/src/index.ts @@ -1,17 +1,18 @@ import { GraderImageBuildRequest, toMilliseconds, + isImageBuildResult } from "@codegrade-orca/common"; -import { getNextImageBuild, handleCompletedImageBuild } from "@codegrade-orca/db"; +import { getNextImageBuild, handleCompletedImageBuild } from "@codegrade-orca/db"; import { createAndStoreGraderImage, removeStaleImageFiles } from "./process-request"; -import { cleanUpDockerFiles, removeImageFromDockerIfExists } from "./utils"; +import { cleanUpDockerFiles, sendJobResultForBuildFail, removeImageFromDockerIfExists, notifyClientOfBuildResult } from "./utils"; const LOOP_SLEEP_TIME = 5; // Seconds const main = async () => { console.info("Build service initialized."); while (true) { - let currentDockerSHASum: string | undefined = undefined; + let infoAsBuildReq: GraderImageBuildRequest | undefined = undefined; try { const nextBuildReq = await getNextImageBuild(); @@ -20,24 +21,34 @@ const main = async () => { continue; } - currentDockerSHASum = nextBuildReq.dockerfileSHA; console.info(`Attempting to build image with SHA ${nextBuildReq.dockerfileSHA}.`); - await createAndStoreGraderImage({ - dockerfileSHASum: nextBuildReq.dockerfileSHA, - dockerfileContents: nextBuildReq.dockerfileContent - } as GraderImageBuildRequest); + infoAsBuildReq = { + dockerfile_sha_sum: nextBuildReq.dockerfileSHA, + dockerfile_contents: nextBuildReq.dockerfileContent, + response_url: nextBuildReq.responseURL, + build_key: nextBuildReq.buildKey + }; + const result = await createAndStoreGraderImage(infoAsBuildReq); await handleCompletedImageBuild(nextBuildReq.dockerfileSHA, true); - console.info(`Successfully build image with SHA ${nextBuildReq.dockerfileSHA}.`); + await notifyClientOfBuildResult(result, infoAsBuildReq); + console.info(`Successfully built image with SHA ${nextBuildReq.dockerfileSHA}.`); } catch (err) { - if (currentDockerSHASum) { - // TODO: Send GradingJobResults back to clients on build failure. - await handleCompletedImageBuild(currentDockerSHASum, false); - await cleanUpDockerFiles(currentDockerSHASum); + if (isImageBuildResult(err) && infoAsBuildReq) { + const cancelledJobInfoList = await handleCompletedImageBuild(infoAsBuildReq.dockerfile_sha_sum, false); + if (cancelledJobInfoList !== null) { + await Promise.all(cancelledJobInfoList.map((cancelInfo) => { + sendJobResultForBuildFail( + cancelInfo, + ).catch((notifyError) => console.error(notifyError)); // At this point we can't really do anything, but we should at least log out what happened. + })); + } + await notifyClientOfBuildResult(err, infoAsBuildReq).catch((notifyError) => console.error(notifyError)); + await cleanUpDockerFiles(infoAsBuildReq.dockerfile_sha_sum); } console.error(err); } finally { - if (currentDockerSHASum) { - await removeImageFromDockerIfExists(currentDockerSHASum); + if (infoAsBuildReq) { + await removeImageFromDockerIfExists(infoAsBuildReq.dockerfile_sha_sum); } await removeStaleImageFiles(); } diff --git a/orchestrator/packages/image-build-service/src/process-request/__tests__/image-creation.test.ts b/orchestrator/packages/image-build-service/src/process-request/__tests__/image-creation.test.ts index dc1345ac..b5243690 100644 --- a/orchestrator/packages/image-build-service/src/process-request/__tests__/image-creation.test.ts +++ b/orchestrator/packages/image-build-service/src/process-request/__tests__/image-creation.test.ts @@ -7,9 +7,10 @@ const CONFIG = getConfig(); describe("grader image functionality", () => { const graderImageBuildReq: GraderImageBuildRequest = { - dockerfileContents: `FROM hello-world - `, - dockerfileSHASum: "generated-sha-sum", + dockerfile_contents: `FROM hello-world`, + dockerfile_sha_sum: "generated-sha-sum", + response_url: "http://example.com/response", + build_key: "{\"grader_id\": 1}" }; beforeAll(() => { @@ -22,7 +23,7 @@ describe("grader image functionality", () => { rmSync( path.join( CONFIG.dockerImageFolder, - `${graderImageBuildReq.dockerfileSHASum}.tgz`, + `${graderImageBuildReq.dockerfile_sha_sum}.tgz`, ), { force: true, @@ -37,7 +38,7 @@ describe("grader image functionality", () => { existsSync( path.join( CONFIG.dockerImageFolder, - `${graderImageBuildReq.dockerfileSHASum}.Dockerfile`, + `${graderImageBuildReq.dockerfile_sha_sum}.Dockerfile`, ), ), ).toBe(false); @@ -46,7 +47,7 @@ describe("grader image functionality", () => { path.join( path.join( CONFIG.dockerImageFolder, - `${graderImageBuildReq.dockerfileSHASum}.tgz`, + `${graderImageBuildReq.dockerfile_sha_sum}.tgz`, ), ), ), diff --git a/orchestrator/packages/image-build-service/src/process-request/image-creation.ts b/orchestrator/packages/image-build-service/src/process-request/image-creation.ts index 497d6737..53ac7123 100644 --- a/orchestrator/packages/image-build-service/src/process-request/image-creation.ts +++ b/orchestrator/packages/image-build-service/src/process-request/image-creation.ts @@ -1,65 +1,84 @@ -import { GraderImageBuildRequest, getConfig } from "@codegrade-orca/common"; +import { GraderImageBuildRequest, GraderImageBuildResult, ImageBuildLog, ImageBuildStep, getConfig } from "@codegrade-orca/common"; import { execFile } from "child_process"; -import { writeFile } from "fs"; -import { rm } from "fs/promises"; +import { writeFile, rm } from "fs"; import path from "path"; const CONFIG = getConfig(); export const createAndStoreGraderImage = ( buildRequest: GraderImageBuildRequest, -) => { - // TODO: validate dockerfileContent - return writeDockerfileContentsToFile(buildRequest) - .then((_) => buildImage(buildRequest)) +): Promise => { + const buildLogs: Array = []; + return writeDockerfileContentsToFile(buildRequest, buildLogs) + .then((_) => buildImage(buildRequest, buildLogs)) .then((_) => - rm( - path.join( - CONFIG.dockerImageFolder, - `${buildRequest.dockerfileSHASum}.Dockerfile`, - ), - ), - ) - .then((_) => saveImageToTgz(buildRequest.dockerfileSHASum)); + removeDockerfileAfterBuild( + path.join(CONFIG.dockerImageFolder, `${buildRequest.dockerfile_sha_sum}.Dockerfile`), + buildLogs + )) + .then((_) => saveImageToTgz(buildRequest.dockerfile_sha_sum, buildLogs)) + .then((_) => ({ logs: buildLogs, was_successful: true })); }; -const writeDockerfileContentsToFile = ({ - dockerfileContents, - dockerfileSHASum, -}: GraderImageBuildRequest): Promise => { +const writeDockerfileContentsToFile = ({ dockerfile_contents, dockerfile_sha_sum }: GraderImageBuildRequest, buildLogs: Array): Promise => { return new Promise((resolve, reject) => { const dockerfilePath = path.join( CONFIG.dockerImageFolder, - `${dockerfileSHASum}.Dockerfile`, + `${dockerfile_sha_sum}.Dockerfile`, ); - writeFile(dockerfilePath, dockerfileContents, (err) => { + writeFile(dockerfilePath, dockerfile_contents, (err) => { + const step: ImageBuildStep = "Write request contents to Dockerfile."; if (err) { - reject(err); + buildLogs.push({ + step, + error: err.toString(), + }); + reject({ + logs: buildLogs, + was_successful: false, + }); } else { - resolve(dockerfilePath); + buildLogs.push({ + step, + output: "Contents written to Dockerfile." + }); + resolve(); } }); }); }; -const buildImage = ({ - dockerfileSHASum, -}: GraderImageBuildRequest): Promise => { +const buildImage = ({ dockerfile_sha_sum }: GraderImageBuildRequest, buildLogs: Array): Promise => { + const dockerBuildArgs = [ + "build", + "-t", + dockerfile_sha_sum, + "-f", + path.join(CONFIG.dockerImageFolder, `${dockerfile_sha_sum}.Dockerfile`), + ".", + ]; return new Promise((resolve, reject) => { execFile( "docker", - [ - "build", - "-t", - dockerfileSHASum, - "-f", - path.join(CONFIG.dockerImageFolder, `${dockerfileSHASum}.Dockerfile`), - ".", - ], - (err, _stdout, _stderr) => { + dockerBuildArgs, + (err, stdout, stderr) => { + const step: ImageBuildStep = "Run docker build on Dockerfile."; if (err) { - reject(err); + buildLogs.push({ + step, + error: stderr + }); + reject({ + was_successful: false, + logs: buildLogs + }); } else { + // All docker build information, regardless of status code, + // is logged to STDERR. + buildLogs.push({ + step, + output: stderr + }); resolve(); } }, @@ -67,20 +86,39 @@ const buildImage = ({ }); }; -const saveImageToTgz = (imageName: string): Promise => { +const removeDockerfileAfterBuild = (dockerfilePath: string, buildLogs: Array): Promise => { + return new Promise((resolve, reject) => { + rm(dockerfilePath, (err) => { + const step: ImageBuildStep = "Remove Dockerfile."; + if (err) { + buildLogs.push({ step, error: err.toString() }); + reject({ was_successful: false, logs: buildLogs }); + } else { + buildLogs.push({ step, output: "Successfully cleaned up Dockerfile." }); + resolve(); + } + }) + }); +}; + +const saveImageToTgz = (imageName: string, buildLogs: Array): Promise => { + const dockerSaveCommandArgs = [ + "save", + "-o", + path.join(CONFIG.dockerImageFolder, `${imageName}.tgz`), + imageName, + ]; return new Promise((resolve, reject) => { execFile( "docker", - [ - "save", - "-o", - path.join(CONFIG.dockerImageFolder, `${imageName}.tgz`), - imageName, - ], - (err, _stdout, _stderr) => { + dockerSaveCommandArgs, + (err, _stdout, stderr) => { + const step: ImageBuildStep = "Save image to .tgz file."; if (err) { - reject(err); + buildLogs.push({ step, error: stderr }); + reject({ was_successful: false, logs: buildLogs }); } else { + buildLogs.push({ step, output: `Successfully saved image to ${imageName}.tgz.` }); resolve(); } }, diff --git a/orchestrator/packages/image-build-service/src/process-request/index.ts b/orchestrator/packages/image-build-service/src/process-request/index.ts index 6a918b8a..50a0b007 100644 --- a/orchestrator/packages/image-build-service/src/process-request/index.ts +++ b/orchestrator/packages/image-build-service/src/process-request/index.ts @@ -2,7 +2,6 @@ import path from "path"; import { getConfig, } from "@codegrade-orca/common"; -import { } from "@codegrade-orca/db"; import { readdir, rm, stat } from "fs/promises"; const CONFIG = getConfig(); diff --git a/orchestrator/packages/image-build-service/src/utils.ts b/orchestrator/packages/image-build-service/src/utils.ts index 7877eefc..7d280c0f 100644 --- a/orchestrator/packages/image-build-service/src/utils.ts +++ b/orchestrator/packages/image-build-service/src/utils.ts @@ -1,4 +1,5 @@ -import { getConfig } from "@codegrade-orca/common"; +import { GraderImageBuildRequest, GraderImageBuildResult, GradingJobResult, getConfig } from "@codegrade-orca/common"; +import { CancelJobInfo } from "@codegrade-orca/db/dist/image-builder-operations/handle-completed-image-build"; import { execFile } from "child_process"; import { existsSync, rmSync } from "fs"; import path from "path"; @@ -35,13 +36,43 @@ const deleteImage = (dockerfileSHASum: string): Promise => { const imageExistsInDocker = (dockerfileSHASum: string): Promise => { return new Promise((resolve, reject) => { - execFile("docker", ["image", "ls", "--format", "{{.Repository}}:{{.Tag}}"],(err, stdout, _stderr) => { - if (err) { - reject(err); - } else { - const images = stdout.split("\n"); - resolve(images.includes(`${dockerfileSHASum}:latest`)); - } + execFile("docker", ["image", "ls", "--format", "{{.Repository}}:{{.Tag}}"], (err, stdout, _stderr) => { + if (err) { + reject(err); + } else { + const images = stdout.split("\n"); + resolve(images.includes(`${dockerfileSHASum}:latest`)); + } }); }); }; + +export const sendJobResultForBuildFail = async (cancelInfo: CancelJobInfo) => { + const result: GradingJobResult = { + shell_responses: [], + errors: ["The grader image for this job failed to build. Please contact a Professor or Admin."] + }; + await fetch(cancelInfo.response_url, { + method: "POST", + headers: { + "Accept": "application/json", + "Content-Type": "application/json" + }, + body: JSON.stringify({ ...result, key: cancelInfo.key }) + }); +} + +export const notifyClientOfBuildResult = async (result: GraderImageBuildResult, originalReq: GraderImageBuildRequest) => { + const { response_url, build_key } = originalReq; + await fetch(response_url, { + method: "POST", + headers: { + "Accept": "application/json", + "Content-Type": "application/json" + }, + body: JSON.stringify({ + ...result, + build_key + }) + }); +} diff --git a/worker/images/testing/echo-server/index.js b/worker/images/testing/echo-server/index.js index 2bcc9dbe..f1187af0 100644 --- a/worker/images/testing/echo-server/index.js +++ b/worker/images/testing/echo-server/index.js @@ -24,10 +24,10 @@ app.get("/job-output/:key", (req, res) => { }); app.post("/job-output", (req, res) => { - const { key, output } = req.body; + const { key, build_key } = req.body; console.log(req.body); - if (key) { - responses[key] = req.body; + if (key || build_key) { + responses[key || build_key] = req.body; return res.sendStatus(200); } res.sendStatus(400); diff --git a/worker/orca_grader/common/grading_job/grading_job_result.py b/worker/orca_grader/common/grading_job/grading_job_result.py index 33d4b14d..48cc5bd0 100644 --- a/worker/orca_grader/common/grading_job/grading_job_result.py +++ b/worker/orca_grader/common/grading_job/grading_job_result.py @@ -1,6 +1,7 @@ from typing import Dict, List, Optional from orca_grader.container.grading_script.grading_script_command_response import GradingScriptCommandResponse -from orca_grader.common.types.grading_job_json_types import GradingJobOutputJSON, GradingJobOutputJSON +from orca_grader.common.types.grading_job_json_types import GradingJobResultJSON + class GradingJobResult: @@ -21,7 +22,7 @@ def get_output(self) -> Optional[str]: def get_execution_errors(self) -> List[Exception]: return self.__execution_errors - def to_json(self, interpolated_dirs: Dict[str, str]) -> GradingJobOutputJSON: + def to_json(self, interpolated_dirs: Dict[str, str]) -> GradingJobResultJSON: result = dict() json_responses = list( map( diff --git a/worker/orca_grader/common/types/grading_job_json_types.py b/worker/orca_grader/common/types/grading_job_json_types.py index 547350eb..333aebb8 100644 --- a/worker/orca_grader/common/types/grading_job_json_types.py +++ b/worker/orca_grader/common/types/grading_job_json_types.py @@ -2,5 +2,5 @@ GradingJobJSON = Dict[Any, Any] GradingScriptCommandJSON = Dict[str, Any] -GradingJobOutputJSON = Dict[str, Any] +GradingJobResultJSON = Dict[str, Any] CodeFileInfoJSON = Dict[str, str]