Skip to content

Commit

Permalink
Merge branch 'main' into feat/image-and-job-status
Browse files Browse the repository at this point in the history
  • Loading branch information
williams-jack committed Jul 21, 2024
2 parents 7de00a0 + 3f494f8 commit 8cdb084
Show file tree
Hide file tree
Showing 25 changed files with 271 additions and 105 deletions.
2 changes: 1 addition & 1 deletion orchestrator/init_dev.sh
Original file line number Diff line number Diff line change
@@ -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
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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}"
};
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -165,7 +166,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) {
Expand Down
19 changes: 19 additions & 0 deletions orchestrator/packages/api/src/controllers/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { GradingJobConfig, GradingJobResult } from "@codegrade-orca/common";
import { Response } from "express";

export const errorResponse = (
Expand All @@ -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}`
));
}
27 changes: 25 additions & 2 deletions orchestrator/packages/common/src/types/image-build-service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,27 @@
export interface GraderImageBuildResult {
was_successful: boolean,
logs: Array<ImageBuildLog>
}

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,
}
1 change: 1 addition & 0 deletions orchestrator/packages/common/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./grading-queue";
export * from "./image-build-service";
export * from "./job-result";
14 changes: 14 additions & 0 deletions orchestrator/packages/common/src/types/job-result.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export interface GradingJobResult {
shell_responses: Array<GradingScriptCommandResponse>,
errors: Array<string>,
output?: string
}

interface GradingScriptCommandResponse {
cmd: string | Array<string>,
stdout: string,
stderr: string,
did_timeout: boolean,
is_error: boolean,
status_code: number
}
Original file line number Diff line number Diff line change
@@ -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}"
};
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ const bashGradingScriptCommand = {
on_complete: {
anyOf: [
{
$ref: "https://orca-schemas.com/grading-job-config/grading-script-command",
type: "array",
items: {
$ref: "https://orca-schemas.com/grading-job-config/grading-script-command",
}
},
{ type: "string" },
{ type: "number" },
Expand All @@ -27,7 +30,10 @@ const bashGradingScriptCommand = {
on_fail: {
anyOf: [
{
$ref: "https://orca-schemas.com/grading-job-config/grading-script-command",
type: "array",
items: {
$ref: "https://orca-schemas.com/grading-job-config/grading-script-command",
}
},
{ type: "string" },
{ type: "number" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand Down Expand Up @@ -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");

Expand Down
4 changes: 4 additions & 0 deletions orchestrator/packages/db/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 4 additions & 2 deletions orchestrator/packages/db/run-migration
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#!/bin/bash

set -e

if [[ "$#" -ne 1 ]]; then
echo "Please pass a *single* argument for migration name."
exit 1
Expand All @@ -8,14 +10,14 @@ fi
migration_name="$1"
container_name='postgres-migration'

docker run --rm -p 5432:5432 -v db-data:/var/lib/postgresql/data \
docker run --rm -p 5434:5432 -v db-data:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=password --name "$container_name" -d postgres:10 > /dev/null

if [[ $? -ne 0 ]]; then
echo "Postgres container failed to start. Aborting."
exit 1
fi

export POSTGRES_URL=postgresql://postgres:password@localhost
export POSTGRES_URL=postgresql://postgres:password@localhost:5434
npx prisma migrate dev --name $1
docker stop "$container_name" > /dev/null
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ import getNextImageBuild from "./get-next-image-build";
import handleCompletedImageBuild from "./handle-completed-image-build";

export { getNextImageBuild };
export { handleCompletedImageBuild };
export { handleCompletedImageBuild };
export * from "./image-build-status";
3 changes: 2 additions & 1 deletion orchestrator/packages/db/src/server-operations/delete-job.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Job> =>
prismaInstance.$transaction(async (tx) => {
const job = await tx.job.findUnique({
where: {
Expand All @@ -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;
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import { GraderImageBuildRequest } from '@codegrade-orca/common';
import prismaInstance from '../prisma-instance';

const enqueueImageBuild = ({ dockerfileSHASum, dockerfileContents }: GraderImageBuildRequest): Promise<boolean> => {
const enqueueImageBuild = ({ dockerfile_sha_sum, dockerfile_contents, response_url, build_key }: GraderImageBuildRequest): Promise<boolean> => {
return prismaInstance.$transaction(async (tx) => {
const buildInfoAlreadyExists = (await tx.imageBuildInfo.count({
where: {
dockerfileSHA: dockerfileSHASum,
dockerfileContent: dockerfileContents
dockerfileSHA: dockerfile_sha_sum,
}
})) > 0;
if (buildInfoAlreadyExists) {
return false;
} 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;
Expand Down
41 changes: 26 additions & 15 deletions orchestrator/packages/image-build-service/src/index.ts
Original file line number Diff line number Diff line change
@@ -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();

Expand All @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand All @@ -22,7 +23,7 @@ describe("grader image functionality", () => {
rmSync(
path.join(
CONFIG.dockerImageFolder,
`${graderImageBuildReq.dockerfileSHASum}.tgz`,
`${graderImageBuildReq.dockerfile_sha_sum}.tgz`,
),
{
force: true,
Expand All @@ -37,7 +38,7 @@ describe("grader image functionality", () => {
existsSync(
path.join(
CONFIG.dockerImageFolder,
`${graderImageBuildReq.dockerfileSHASum}.Dockerfile`,
`${graderImageBuildReq.dockerfile_sha_sum}.Dockerfile`,
),
),
).toBe(false);
Expand All @@ -46,7 +47,7 @@ describe("grader image functionality", () => {
path.join(
path.join(
CONFIG.dockerImageFolder,
`${graderImageBuildReq.dockerfileSHASum}.tgz`,
`${graderImageBuildReq.dockerfile_sha_sum}.tgz`,
),
),
),
Expand Down
Loading

0 comments on commit 8cdb084

Please sign in to comment.