diff --git a/package.json b/package.json index e7724c5..1456510 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "test:e2e": "yarn workspace tests test", "analytics": "yarn workspace analytics dev", "setup": "yarn workspace server setup", - "clean": "yarn workspace server clean" + "clean": "yarn workspace server clean", + "build:image": "yarn workspace server build" }, "devDependencies": { "@vue/eslint-config-prettier": "^7.1.0", diff --git a/quests/helloworld/Dockerfile b/quests/helloworld/Dockerfile index f053a50..fe63a59 100644 --- a/quests/helloworld/Dockerfile +++ b/quests/helloworld/Dockerfile @@ -1,3 +1,3 @@ FROM lancatlin/linux-odyssey:base -COPY ./home ${HOME} +COPY home ${HOME} diff --git a/quests/spell/Dockerfile b/quests/spell/Dockerfile index f053a50..fe63a59 100644 --- a/quests/spell/Dockerfile +++ b/quests/spell/Dockerfile @@ -1,3 +1,3 @@ FROM lancatlin/linux-odyssey:base -COPY ./home ${HOME} +COPY home ${HOME} diff --git a/server/package.json b/server/package.json index f65ca21..0c4e5f8 100644 --- a/server/package.json +++ b/server/package.json @@ -8,7 +8,8 @@ "dev": "nodemon src/index.js", "test": "yarn node --experimental-vm-modules $(yarn bin jest)", "setup": "node src/cli/setup.js", - "clean": "node src/cli/clean.js" + "clean": "node src/cli/clean.js", + "build": "node src/cli/build.js" }, "dependencies": { "@linux-odyssey/constants": "*", diff --git a/server/src/cli/build.js b/server/src/cli/build.js new file mode 100644 index 0000000..41c95d4 --- /dev/null +++ b/server/src/cli/build.js @@ -0,0 +1,24 @@ +import { getQuestDockerfiles } from '../utils/quest.js' +import { buildQuestImage } from '../containers/docker.js' + +async function buildImages() { + const quests = await getQuestDockerfiles() + console.log(`Building ${quests.length} images`) + return Promise.all( + quests.map(({ id, questPath }) => { + console.log(`Building image for ${id}`) + return buildQuestImage(questPath, id) + }) + ) +} + +buildImages() + .then((result) => { + console.log(result) + console.log('Done!') + process.exit(0) + }) + .catch((err) => { + console.error(err) + process.exit(1) + }) diff --git a/server/src/containers/docker.js b/server/src/containers/docker.js index b1485ce..42a386a 100644 --- a/server/src/containers/docker.js +++ b/server/src/containers/docker.js @@ -78,3 +78,41 @@ export async function deleteContainer(id) { } await container.remove() } + +export function buildQuestImage(questPath, questId) { + return new Promise((resolve, reject) => { + engine.buildImage( + { + context: questPath, + src: ['Dockerfile', 'home'], + }, + { t: `${config.dockerImage}:${questId}` }, + (err, response) => { + if (err) { + reject(err) + } + response.on('data', (data) => { + // Process the data (this could be Docker build output) + const { stream, error } = JSON.parse(data.toString()) + if (stream) { + console.log(stream) + } + if (error) { + console.error(error) + reject(error) + } + }) + + response.on('end', () => { + console.log(`Build completed for ${questId}`) + resolve(questId) + }) + + response.on('error', (error) => { + console.error(`Build failed for ${questId}:`, error) + reject(error) + }) + } + ) + }) +} diff --git a/server/src/index.js b/server/src/index.js index 52c2d88..fda82b1 100644 --- a/server/src/index.js +++ b/server/src/index.js @@ -9,7 +9,7 @@ import connectDB from '@linux-odyssey/models' import './auth/passport.js' import socketServer from './api/socket.js' import apiRouter from './api/routes/index.js' -import loadAndUpdateQuests from './utils/quest.js' +import { loadAndUpdateQuests } from './utils/quest.js' import config from './config.js' import errorHandler from './middleware/error.js' import { globalRateLimit } from './middleware/rateLimiter.js' diff --git a/server/src/utils/quest.js b/server/src/utils/quest.js index b189059..5b8a3fc 100644 --- a/server/src/utils/quest.js +++ b/server/src/utils/quest.js @@ -6,51 +6,60 @@ import { Quest } from '@linux-odyssey/models' const questDirectory = path.join(process.cwd(), '..', 'quests') -export default async function loadAndUpdateQuests() { +async function mapQuests(callback) { const questNames = await fs.readdir(questDirectory, { withFileTypes: true, }) - const quests = Promise.all( + return Promise.all( questNames .filter((dirent) => dirent.isDirectory() && !dirent.name.startsWith('.')) - .map(async (dirent) => { + .map((dirent) => { const id = dirent.name const questPath = path.join(questDirectory, id) - const files = await fs.readdir(questPath) - if (!files.includes('game.yml')) { - throw new Error(`Quest ${id} is missing game.yml`) - } - - try { - const body = await fs.readFile( - path.join(questPath, 'game.yml'), - 'utf-8' - ) - - const quest = yaml.parse(body, { - merge: true, - }) - - const image = files.includes('Dockerfile') ? id : 'base' - - return Quest.findByIdAndUpdate( - id, - { - _id: id, - image, - ...quest, - }, - { - upsert: true, - } - ) - } catch (error) { - console.error(`Error parsing quest ${id}:`, error) - throw error - } + return callback(id, questPath) }) ) +} - return quests +export function loadAndUpdateQuests() { + return mapQuests(async (id, questPath) => { + const files = await fs.readdir(questPath) + if (!files.includes('game.yml')) { + throw new Error(`Quest ${id} is missing game.yml`) + } + + try { + const body = await fs.readFile(path.join(questPath, 'game.yml'), 'utf-8') + + const quest = yaml.parse(body, { + merge: true, + }) + + const image = files.includes('Dockerfile') ? id : 'base' + + return Quest.findByIdAndUpdate( + id, + { + _id: id, + image, + ...quest, + }, + { + upsert: true, + } + ) + } catch (error) { + console.error(`Error parsing quest ${id}:`, error) + throw error + } + }) +} + +export async function getQuestDockerfiles() { + const quests = await mapQuests(async (id, questPath) => { + const files = await fs.readdir(questPath) + return files.includes('Dockerfile') ? { id, questPath } : null + }) + return quests.filter((quest) => quest !== null) }