Skip to content

Commit

Permalink
Merge pull request codecrafters-io#38 from codecrafters-io/add-docker…
Browse files Browse the repository at this point in the history
…file-processing

Fix Dockerfile processing and build command in DockerfileTester
  • Loading branch information
rohitpaulk authored Aug 16, 2024
2 parents f6df13c + 8d2cc81 commit dd9950b
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 16 deletions.
8 changes: 8 additions & 0 deletions lib/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ export class InvalidCourseDefinitionFileError extends FriendlyError {
}
}

export class InvalidDockerfileContentsError extends FriendlyError {
constructor(jsonResponse: string) {
super(
`CodeCrafters was unable to process this Dockerfile. Error: ${jsonResponse}. \n\nThink this is a mistake? Please file an issue at https://github.com/codecrafters-io/course-sdk/issues`
);
}
}

export class LanguageTemplateNotAvailableError extends FriendlyError {
constructor(language: Language) {
super(
Expand Down
26 changes: 25 additions & 1 deletion lib/models/course.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import CourseStage from "./course-stage";
import child_process from "child_process";
import Dockerfile from "./dockerfile";
import YAML from "js-yaml";
import fs from "fs";
Expand All @@ -11,6 +12,9 @@ import {
StarterTemplateConfigFileNotFoundError,
} from "../errors";
import Language from "./language";
import tmp from "tmp";
import util from "util";
const exec = util.promisify(child_process.exec);

export default class Course {
slug: string;
Expand Down Expand Up @@ -125,7 +129,7 @@ export default class Course {

for (const languageSlug in dockerfilesGroupedByLanguageSlug) {
const dockerfiles = dockerfilesGroupedByLanguageSlug[languageSlug];
const latestDockerfile = dockerfiles.sort((a, b) => b.semver().compare(a.semver()))[0];
const latestDockerfile = dockerfiles.sort((a, b) => b.semver.compare(a.semver))[0];
latestDockerfiles.push(latestDockerfile);
}

Expand All @@ -136,6 +140,26 @@ export default class Course {
return path.join(this.compiledStarterRepositoriesDir, language.slug);
}

// Prepares repository dir for language
async prepareRepositoryDirForLanguage(language: Language): Promise<string> {
const repositoryDir = tmp.dirSync().name;
await exec(`rm -rf ${repositoryDir}`);
await exec(`cp -r ${this.compiledStarterRepositoryDirForLanguage(language)} ${repositoryDir}`);

await exec(`git -C ${repositoryDir} init`);
await exec(`git -C ${repositoryDir} add .`);
await exec(`git -C ${repositoryDir} commit -m "Initial commit"`);

// Test runner binary
await exec(`mkdir -p ${repositoryDir}/test-runner`);
await exec(`touch ${repositoryDir}/test-runner/test-runner`);

// Tester directory
await exec(`mkdir -p ${repositoryDir}/tester`);

return repositoryDir;
}

latestDockerfileForLanguage(language: Language): Dockerfile | undefined {
return this.latestDockerfiles.find((dockerfile) => dockerfile.language.slug === language.slug);
}
Expand Down
49 changes: 48 additions & 1 deletion lib/models/dockerfile.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import Language from "./language";
import semver from "semver";
import path from "path";
import fs from "fs";
import tmp from "tmp";
import { InvalidDockerfileContentsError } from "../errors";

export default class Dockerfile {
path: string;
_processedContentsCache?: string;

constructor(path: string) {
this.path = path;
}

get contents(): string {
return fs.readFileSync(this.path, "utf8");
}

get filename() {
return path.basename(this.path);
}
Expand All @@ -29,7 +37,22 @@ export default class Dockerfile {
return Language.findByLanguagePack(this.languagePack);
}

semver(): semver.SemVer {
get processedContents(): string {
if (!this._processedContentsCache) {
throw new Error("processContents was not called!");
}

return this._processedContentsCache;
}

get processedPath(): string {
const tmpFile = tmp.fileSync({ postfix: ".Dockerfile" });
fs.writeFileSync(tmpFile.name, this.processedContents);

return tmpFile.name;
}

get semver(): semver.SemVer {
const versionString = this.languagePackWithVersion.replace(`${this.language.slug}-`, "");

if (semver.coerce(versionString) === null) {
Expand All @@ -38,4 +61,28 @@ export default class Dockerfile {

return semver.coerce(versionString) as semver.SemVer;
}

async processContents() {
const response = await fetch("https://paul-backend.ccdev.dev/services/course_sdk/process_dockerfile", {
// const response = await fetch("https://backend.codecrafters.io/services/course_sdk/process_dockerfile", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
dockerfile_contents: this.contents,
}),
});

if (response.status == 422) {
throw new InvalidDockerfileContentsError(await response.text());
}

if (response.status !== 200) {
throw new Error(`Failed to process Dockerfile. Status: ${response.status}, Response: ${await response.text()}`);
}

const responseBody = (await response.json()) as { dockerfile_contents: string };
this._processedContentsCache = responseBody.dockerfile_contents;
}
}
1 change: 1 addition & 0 deletions lib/testers/command-tester.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Dockerfile from "../models/dockerfile";

const exec = util.promisify(child_process.exec);

// TODO: Make this work with dockerfile#processedContents
export default class CommandTester extends BaseTester {
course: Course;
dockerfile: Dockerfile;
Expand Down
11 changes: 5 additions & 6 deletions lib/testers/dockerfile-tester.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,11 @@ export default class DockerfileTester extends BaseTester {
}

async doTest() {
this.copiedStarterDir = tmp.dirSync().name;
await exec(`rm -rf ${this.copiedStarterDir}`);
await exec(`cp -r ${this.starterDir} ${this.copiedStarterDir}`);

Logger.logHeader(`Testing Dockerfile: ${this.slug}`);

await this.dockerfile.processContents();
this.copiedStarterDir = await this.course.prepareRepositoryDirForLanguage(this.language);

Logger.logInfo(`Building ${this.dockerfile.languagePackWithVersion} image without cache`);
const timeTaken = await this.assertTimeUnder(400, this.buildImage.bind(this));

Expand All @@ -45,7 +44,7 @@ export default class DockerfileTester extends BaseTester {
}

async buildImage() {
const command = `docker build -t ${this.slug} -f ${this.dockerfile.path} ${this.copiedStarterDir}`;
const command = `docker build -t ${this.slug} -f ${this.dockerfile.processedPath} ${this.copiedStarterDir}`;
const expectedOutput = `naming to docker.io/library/${this.slug}`;
await this.assertStderrContains(command, expectedOutput);
}
Expand All @@ -55,6 +54,6 @@ export default class DockerfileTester extends BaseTester {
}

get starterDir() {
return `${this.course.compiledStarterRepositoriesDir}/${this.language.slug}`;
return `${this.course.compiledStarterRepositoryDirForLanguage(this.language)}`;
}
}
23 changes: 15 additions & 8 deletions lib/testers/starter-code-tester.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import StarterCodeUncommenter from "../starter-code-uncommenter";
import LineWithCommentRemover from "../line-with-comment-remover";
import testScriptFile from "../scripts/test.sh";
import ShellCommandExecutor from "../shell-command-executor";
import Dockerfile from "../models/dockerfile";

const writeFile = util.promisify(fs.writeFile);

Expand All @@ -22,6 +23,7 @@ export default class StarterCodeTester extends BaseTester {
course: Course;
language: Language;
testerDir: string;
_dockerfile?: Dockerfile;

copiedStarterDir: string | undefined;

Expand All @@ -33,12 +35,8 @@ export default class StarterCodeTester extends BaseTester {
}

async doTest() {
this.copiedStarterDir = tmp.dirSync().name;
await exec(`rm -rf ${this.copiedStarterDir}`);
await exec(`cp -r ${this.starterDir} ${this.copiedStarterDir}`);
await exec(`git -C ${this.copiedStarterDir} init`);
await exec(`git -C ${this.copiedStarterDir} add .`);
await exec(`git -C ${this.copiedStarterDir} commit -m "Initial commit"`);
this.copiedStarterDir = await this.course.prepareRepositoryDirForLanguage(this.language);
await this.dockerfile!.processContents();

Logger.logHeader(`Testing starter: ${this.course.slug}-${this.language.slug}`);

Expand Down Expand Up @@ -90,6 +88,10 @@ export default class StarterCodeTester extends BaseTester {
Logger.logInfo("Restoring changes to .sh files");
await ShellCommandExecutor.execute(`git -C ${this.copiedStarterDir} restore *.sh`); // Hack to work around our precompilation step mangling .sh files

Logger.logInfo("Removing test-runner & tester"); // We use this for the tester directories
await ShellCommandExecutor.execute(`rm -rf ${this.copiedStarterDir}/test-runner`);
await ShellCommandExecutor.execute(`rm -rf ${this.copiedStarterDir}/tester`);

const diff = await ShellCommandExecutor.execute(`git -C ${this.copiedStarterDir} diff --exit-code`, { expectedExitCodes: [0, 1] });

if (diff.exitCode === 0) {
Expand Down Expand Up @@ -154,8 +156,13 @@ export default class StarterCodeTester extends BaseTester {
Logger.logSuccess(`Took ${timeTaken} secs`);
}

// Cache so that Dockerfile.processedContents is stored
get dockerfile() {
return this.course.latestDockerfiles.find((dockerfile) => dockerfile.languagePack === this.languagePack);
if (!this._dockerfile) {
this._dockerfile = this.course.latestDockerfiles.find((dockerfile) => dockerfile.languagePack === this.languagePack);
}

return this._dockerfile;
}

get slug() {
Expand All @@ -171,7 +178,7 @@ export default class StarterCodeTester extends BaseTester {
}

async buildImage() {
const command = `docker build -t ${this.slug} -f ${this.dockerfile!.path} ${this.copiedStarterDir}`;
const command = `docker build -t ${this.slug} -f ${this.dockerfile!.processedPath} ${this.copiedStarterDir}`;
const expectedOutput = `naming to docker.io/library/${this.slug}`;
await this.assertStderrContains(command, expectedOutput);
}
Expand Down

0 comments on commit dd9950b

Please sign in to comment.