From fe3f4ddc7359015f354734fd9e46e313c6398d91 Mon Sep 17 00:00:00 2001 From: wancat Date: Mon, 24 Jul 2023 16:00:44 +0800 Subject: [PATCH 01/11] fix: docker image name update --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 8900c1a..8840e9d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: '3' services: app: - image: lancatlin/linux-odyssey-server + image: lancatlin/linux-odyssey build: . ports: - '3000:3000' From 3cea54ce71d8a1046b27fef4dcb30a6bf4a6b0c6 Mon Sep 17 00:00:00 2001 From: wancat Date: Mon, 24 Jul 2023 16:04:26 +0800 Subject: [PATCH 02/11] feat: add new API for recording executed commands - Add the `jsonwebtoken` dependency to the server's `package.json` - Add a new file `commandController.js` to the server's API controllers - Add a new route `/commands` to the server's API routes - Add a new file `commands.js` to the server's API routes - Modify the `index.js` file in the server's API routes to import and use the `commands.js` route - Add a `newCommand` function to the `commandController.js` file - Add a `Command` model to the `command.js` file in the server's models - Add the `genSessionJWT` and `verifySessionJWT` functions to the `auth.js` file in the server's utils - Modify the `attachContainer` function in the `docker.js` file in the server's utils to include a `token` parameter - Modify the `swagger.yaml` file to include a new route `/commands` for recording executed commands in a session Signed-off-by: wancat --- server/package.json | 1 + .../src/api/controllers/commandController.js | 35 +++++++++++++++ server/src/api/routes/commands.js | 8 ++++ server/src/api/routes/index.js | 2 + server/src/api/socket.js | 6 ++- server/src/config.js | 1 + server/src/models/command.js | 29 +++++++++++++ server/src/utils/auth.js | 32 +++++++++++++- server/src/utils/docker.js | 3 +- server/swagger.yaml | 40 +++++++++++++++++ yarn.lock | 43 ++++++++++++++++++- 11 files changed, 194 insertions(+), 6 deletions(-) create mode 100644 server/src/api/controllers/commandController.js create mode 100644 server/src/api/routes/commands.js create mode 100644 server/src/models/command.js diff --git a/server/package.json b/server/package.json index 2c70405..b5ec08a 100644 --- a/server/package.json +++ b/server/package.json @@ -12,6 +12,7 @@ "dependencies": { "dockerode": "^3.3.5", "express": "^4.18.2", + "jsonwebtoken": "^9.0.1", "marked": "^5.1.1", "mongoose": "^7.4.0", "socket.io": "^4.7.1", diff --git a/server/src/api/controllers/commandController.js b/server/src/api/controllers/commandController.js new file mode 100644 index 0000000..5f8a887 --- /dev/null +++ b/server/src/api/controllers/commandController.js @@ -0,0 +1,35 @@ +/* eslint-disable import/prefer-default-export */ +import Command from '../../models/command.js' +import Session from '../../models/session.js' +import { verifySessionJWT } from '../../utils/auth.js' + +export async function newCommand(req, res) { + const { token, command, pwd, output } = req.body + + let sessionId + try { + const decoded = await verifySessionJWT(token) + sessionId = decoded.session_id + } catch (err) { + res.status(401).json({ message: 'token not valid' }) + return + } + + const session = await Session.findById(sessionId) + + if (!session) { + res.status(401).json({ message: 'session not found' }) + return + } + + const c = new Command({ + session, + command, + pwd, + output, + }) + + await c.save() + + res.status(201).end() +} diff --git a/server/src/api/routes/commands.js b/server/src/api/routes/commands.js new file mode 100644 index 0000000..5eeef05 --- /dev/null +++ b/server/src/api/routes/commands.js @@ -0,0 +1,8 @@ +import { Router } from 'express' +import * as commandController from '../controllers/commandController.js' + +const router = Router() + +router.post('/', commandController.newCommand) + +export default router diff --git a/server/src/api/routes/index.js b/server/src/api/routes/index.js index bc3eae8..e7c6084 100644 --- a/server/src/api/routes/index.js +++ b/server/src/api/routes/index.js @@ -1,6 +1,7 @@ import { Router } from 'express' import sessions from './sessions.js' import quests from './quests.js' +import commands from './commands.js' import { authMiddleware } from '../../middleware/auth.js' const router = Router() @@ -11,5 +12,6 @@ router.get('/', (req, res) => { router.use('/sessions', authMiddleware, sessions) router.use('/quests', quests) +router.use('/commands', commands) export default router diff --git a/server/src/api/socket.js b/server/src/api/socket.js index 5f08973..e5d4bc4 100644 --- a/server/src/api/socket.js +++ b/server/src/api/socket.js @@ -1,7 +1,7 @@ import { Server } from 'socket.io' import { getOrCreateContainer, attachContainer } from '../utils/docker.js' import Session from '../models/session.js' -import { defaultUser } from '../utils/auth.js' +import { defaultUser, genSessionJWT } from '../utils/auth.js' export default (server) => { const io = new Server(server) @@ -32,8 +32,10 @@ export default (server) => { return } + const token = await genSessionJWT(session) + const container = await getOrCreateContainer(session.containerId) - const stream = await attachContainer(container) + const stream = await attachContainer(container, { token }) stream.on('data', (chunk) => { socket.send(chunk) diff --git a/server/src/config.js b/server/src/config.js index 0aaef88..7304fb5 100644 --- a/server/src/config.js +++ b/server/src/config.js @@ -10,4 +10,5 @@ export default { host: get('HOST', 'localhost'), port: get('PORT', 3000), db: get('MONGO_URL', 'mongodb://localhost:27017/odyssey-test'), + jwtSecret: get('JWT_SECRET', 'secret'), } diff --git a/server/src/models/command.js b/server/src/models/command.js new file mode 100644 index 0000000..4ea6697 --- /dev/null +++ b/server/src/models/command.js @@ -0,0 +1,29 @@ +import { model, Schema } from 'mongoose' + +const Command = model( + 'Command', + new Schema( + { + session: { + type: Schema.ObjectId, + ref: 'Session', + required: true, + }, + pwd: { + type: String, + required: true, + }, + command: { + type: String, + required: true, + }, + output: { + type: String, + required: false, + }, + }, + { timestamps: true } + ) +) + +export default Command diff --git a/server/src/utils/auth.js b/server/src/utils/auth.js index e33cf5f..d97de3a 100644 --- a/server/src/utils/auth.js +++ b/server/src/utils/auth.js @@ -1,4 +1,5 @@ -/* eslint-disable import/prefer-default-export */ +import jwt from 'jsonwebtoken' +import config from '../config.js' import User from '../models/user.js' export async function defaultUser() { @@ -12,3 +13,32 @@ export async function defaultUser() { } return user } + +export function genSessionJWT(session) { + return new Promise((resolve, reject) => { + jwt.sign( + { session_id: session.id }, + config.jwtSecret, + { expiresIn: '1h' }, + (err, token) => { + if (err) { + reject(err) + } else { + resolve(token) + } + } + ) + }) +} + +export function verifySessionJWT(token) { + return new Promise((resolve, reject) => { + jwt.verify(token, config.jwtSecret, (err, decoded) => { + if (err) { + reject(err) + } else { + resolve(decoded) + } + }) + }) +} diff --git a/server/src/utils/docker.js b/server/src/utils/docker.js index a78b738..449bf4b 100644 --- a/server/src/utils/docker.js +++ b/server/src/utils/docker.js @@ -46,7 +46,7 @@ export async function getOrCreateContainer(id) { } } -export async function attachContainer(container) { +export async function attachContainer(container, { token }) { // Create an exec instance with bash shell const exec = await container.exec({ AttachStdin: true, @@ -54,6 +54,7 @@ export async function attachContainer(container) { AttachStderr: true, Cmd: ['/bin/bash'], Tty: true, + Env: [`TOKEN=${token}`], }) const execOutput = await exec.start({ diff --git a/server/swagger.yaml b/server/swagger.yaml index d33f72a..2527225 100644 --- a/server/swagger.yaml +++ b/server/swagger.yaml @@ -291,6 +291,24 @@ paths: properties: message: type: string + + /commands: + post: + summary: Record a command that was executed in a session + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Command' + responses: + '201': + description: Command recorded successfully + '400': + description: Invalid request body + '401': + description: Unauthorized + components: schemas: User: @@ -422,6 +440,28 @@ components: source: type: string + Command: + type: object + required: + - token + - pwd + - command + properties: + token: + type: string + description: The JWT token of the session + pwd: + type: string + example: '/root' + description: The current working directory + command: + type: string + example: 'ls -al /' + description: The command that was executed + output: + type: string + description: The output of the command + securitySchemes: BearerAuth: type: http diff --git a/yarn.lock b/yarn.lock index b19df92..3d1cf5e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1323,6 +1323,11 @@ bson@^5.4.0: resolved "https://registry.yarnpkg.com/bson/-/bson-5.4.0.tgz#0eea77276d490953ad8616b483298dbff07384c6" integrity sha512-WRZ5SQI5GfUuKnPTNmAYPiKIof3ORXAF4IRU5UcgmivNIon01rWQlw5RUH954dpu8yGL8T59YShVddIPaU/gFA== +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" @@ -1698,6 +1703,13 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -3218,6 +3230,33 @@ json5@^2.2.2: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== +jsonwebtoken@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.1.tgz#81d8c901c112c24e497a55daf6b2be1225b40145" + integrity sha512-K8wx7eJ5TPvEjuiVSkv167EVboBDv9PZdDoF7BgeQnBLVvZWW9clr2PsQHVJDTKaEIH5JBIwHujGcHp7GgI2eg== + dependencies: + jws "^3.2.2" + lodash "^4.17.21" + ms "^2.1.1" + semver "^7.3.8" + +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + kareem@2.5.1: version "2.5.1" resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.5.1.tgz#7b8203e11819a8e77a34b3517d3ead206764d15d" @@ -4010,7 +4049,7 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -safe-buffer@5.2.1, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -4041,7 +4080,7 @@ semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.5, semver@^7.3.6, semver@^7.5.3: +semver@^7.3.5, semver@^7.3.6, semver@^7.3.8, semver@^7.5.3: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== From 189c87df5af70c3ab07bf976789b00d0e33afbdd Mon Sep 17 00:00:00 2001 From: wancat Date: Tue, 25 Jul 2023 15:36:52 +0800 Subject: [PATCH 03/11] build: setup Docker dev configuration - Add `.dockerignore` file - Add `Dockerfile.dev` - Modify `docker-compose.yml`: - Add `restart: on-failure` to `app` service - Modify `build` section of `app` service - Remove `links` section from `app` service - Add `networks` section to `app` and `db` services - Add `networks` section to the end of the file Signed-off-by: wancat --- .dockerignore | 2 ++ Dockerfile.dev | 15 +++++++++++++++ docker-compose.yml | 19 ++++++++++++++++--- 3 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile.dev diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d992b0f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +node_modules +.git \ No newline at end of file diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..ae536bb --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,15 @@ +FROM node + +WORKDIR /app +COPY package.json yarn.lock ./ +COPY server/package.json ./server/ +RUN yarn install --frozen-lockfile + +WORKDIR /app/server + +ENV NODE_ENV=development +ENV HOST=0.0.0.0 +ENV PORT=3000 +EXPOSE 3000 + +CMD [ "yarn", "dev" ] diff --git a/docker-compose.yml b/docker-compose.yml index 8840e9d..7cdeb90 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,19 +2,32 @@ version: '3' services: app: image: lancatlin/linux-odyssey - build: . + restart: on-failure + build: + context: . + dockerfile: Dockerfile.dev + networks: + - internal + - players ports: - '3000:3000' - links: - - db volumes: - /var/run/docker.sock:/var/run/docker.sock + - ./server:/app/server environment: - MONGO_URL=mongodb://db:27017/odyssey-test + - DOCKER_NETWORK=linux-odyssey-players db: image: mongo restart: always + networks: + - internal ports: - '27017:27017' volumes: - ./data:/data/db + +networks: + internal: + players: + name: linux-odyssey-players From 943c11714c7fd8ebef0680393e4be3487eaac61d Mon Sep 17 00:00:00 2001 From: wancat Date: Tue, 25 Jul 2023 15:41:14 +0800 Subject: [PATCH 04/11] feat: connect containers to custom network - Export the `get` function in `utils/env.js` - Change the Docker image in `docker.js` from `ubuntu` to `lancatlin/quest-helloworld` - Remove the `createContainer` and `getContainer` functions from `docker.js` - Add a `network` constant in `docker.js` that gets the Docker network from the config - Connect the container to the network in `getOrCreateContainer` function in `docker.js` - Add `API_ENDPOINT` environment variable in the `attachContainer` function in `docker.js` Signed-off-by: wancat --- server/src/config.js | 9 ++------- server/src/utils/docker.js | 21 ++++++--------------- server/src/utils/env.js | 7 +++++++ 3 files changed, 15 insertions(+), 22 deletions(-) create mode 100644 server/src/utils/env.js diff --git a/server/src/config.js b/server/src/config.js index 7304fb5..a228473 100644 --- a/server/src/config.js +++ b/server/src/config.js @@ -1,14 +1,9 @@ -function get(key, defaultValue) { - const value = process.env[key] - if (value != null) { - return value - } - return defaultValue -} +import { get } from './utils/env.js' export default { host: get('HOST', 'localhost'), port: get('PORT', 3000), db: get('MONGO_URL', 'mongodb://localhost:27017/odyssey-test'), jwtSecret: get('JWT_SECRET', 'secret'), + dockerNetwork: get('DOCKER_NETWORK', 'linux-odyssey-players'), } diff --git a/server/src/utils/docker.js b/server/src/utils/docker.js index 449bf4b..7f5189b 100644 --- a/server/src/utils/docker.js +++ b/server/src/utils/docker.js @@ -1,9 +1,10 @@ import Docker from 'dockerode' +import config from '../config.js' const engine = new Docker() const containerOptions = { - Image: 'ubuntu', + Image: 'lancatlin/quest-helloworld', AttachStdin: true, AttachStdout: true, AttachStderr: true, @@ -13,19 +14,7 @@ const containerOptions = { StdinOnce: false, } -export async function createContainer() { - const container = await engine.createContainer({ - ...containerOptions, - }) - await container.start() - return container -} - -export async function getContainer(id) { - const container = await engine.getContainer(id) - await container.start() - return container -} +const network = engine.getNetwork(config.dockerNetwork) export async function getOrCreateContainer(id) { console.log(`Getting container: ${id}`) @@ -40,6 +29,8 @@ export async function getOrCreateContainer(id) { ...containerOptions, name: id, }) + await network.connect({ Container: container.id }) + await container.start() console.log(container.id) return container @@ -54,7 +45,7 @@ export async function attachContainer(container, { token }) { AttachStderr: true, Cmd: ['/bin/bash'], Tty: true, - Env: [`TOKEN=${token}`], + Env: [`TOKEN=${token}`, `API_ENDPOINT=http://app:3000`], }) const execOutput = await exec.start({ diff --git a/server/src/utils/env.js b/server/src/utils/env.js new file mode 100644 index 0000000..2db79cc --- /dev/null +++ b/server/src/utils/env.js @@ -0,0 +1,7 @@ +export function get(key, defaultValue) { + const value = process.env[key] + if (value != null) { + return value + } + return defaultValue +} From 88ed447573d21c95f2e42e5d3071e8c064a63f69 Mon Sep 17 00:00:00 2001 From: wancat Date: Tue, 25 Jul 2023 15:46:55 +0800 Subject: [PATCH 05/11] fix: error handling - Remove unnecessary try-catch block in createSession function - Add error handling for missing session ID in socket.js - Improve error handling in connectDB function - Add error handling middleware for server errors - Add default export for get function in env.js Signed-off-by: wancat --- .../src/api/controllers/sessionController.js | 12 ++++----- server/src/api/socket.js | 26 ++++++++++++++----- server/src/db.js | 3 ++- server/src/index.js | 3 +++ server/src/middleware/error.js | 5 ++++ server/src/utils/env.js | 1 + 6 files changed, 36 insertions(+), 14 deletions(-) diff --git a/server/src/api/controllers/sessionController.js b/server/src/api/controllers/sessionController.js index 19d431d..81e163f 100644 --- a/server/src/api/controllers/sessionController.js +++ b/server/src/api/controllers/sessionController.js @@ -22,13 +22,13 @@ export async function createSession(req, res) { return } - const newSession = new Session({ - user: req.user, - quest, - containerId: `quest-${quest.id}-${req.user.username}-${Date.now()}`, - }) - try { + const newSession = new Session({ + user: req.user, + quest, + containerId: `quest-${quest.id}-${req.user.username}-${Date.now()}`, + }) + const session = await newSession.save() res.status(201).json(session) } catch (err) { diff --git a/server/src/api/socket.js b/server/src/api/socket.js index e5d4bc4..24a75e7 100644 --- a/server/src/api/socket.js +++ b/server/src/api/socket.js @@ -21,13 +21,25 @@ export default (server) => { io.on('connection', async (socket) => { console.log('Connected to the client.') - const session = await Session.findOne({ - _id: socket.handshake.query.session_id, - user: socket.user, - }) - - if (!session) { - socket.send('Session not found.') + const sessionId = socket.handshake.query.session_id + if (!sessionId) { + socket.send('Session ID not found.') + socket.disconnect() + return + } + let session + try { + session = await Session.findOne({ + _id: sessionId, + user: socket.user, + }) + if (!session) { + socket.send('Session not found.') + socket.disconnect() + return + } + } catch (err) { + socket.send(err.message) socket.disconnect() return } diff --git a/server/src/db.js b/server/src/db.js index fb575c9..842ce6f 100644 --- a/server/src/db.js +++ b/server/src/db.js @@ -6,6 +6,7 @@ export default async function connectDB() { await mongoose.connect(config.db) console.log('Connected to MongoDB') } catch (err) { - console.log(err) + console.error(err) + process.exit(1) } } diff --git a/server/src/index.js b/server/src/index.js index 80a424e..4b5ad47 100644 --- a/server/src/index.js +++ b/server/src/index.js @@ -9,6 +9,7 @@ import connectDB from './db.js' import apiRouter from './api/routes/index.js' import loadAndUpdateQuests from './utils/quest.js' import config from './config.js' +import errorHandler from './middleware/error.js' async function main() { const app = express() @@ -28,6 +29,8 @@ async function main() { app.use('/api/v1', apiRouter) + app.use(errorHandler) + server.listen(config.port, config.host, () => { console.log(`Server listening at http://${config.host}:${config.port}`) }) diff --git a/server/src/middleware/error.js b/server/src/middleware/error.js index e69de29..fc1be7e 100644 --- a/server/src/middleware/error.js +++ b/server/src/middleware/error.js @@ -0,0 +1,5 @@ +// eslint-disable-next-line no-unused-vars +export default function errorHandler(err, req, res, next) { + console.error(err.stack) // Log error stack trace to the console + res.status(500).send({ message: err.message }) // Send error response +} diff --git a/server/src/utils/env.js b/server/src/utils/env.js index 2db79cc..3195d31 100644 --- a/server/src/utils/env.js +++ b/server/src/utils/env.js @@ -1,3 +1,4 @@ +/* eslint-disable import/prefer-default-export */ export function get(key, defaultValue) { const value = process.env[key] if (value != null) { From 9f74a805517811c1573e6b5a3a2a640cb9941121 Mon Sep 17 00:00:00 2001 From: wancat Date: Tue, 25 Jul 2023 15:56:51 +0800 Subject: [PATCH 06/11] feat: refactor session management and API usage - Add an import statement for the `get` function from `./utils/env.js` - Add a constant for `API_ENDPOINT` using the `get` function - Remove the `sessionId` constant declaration - Add an `async` function `getSessionId` for fetching session ID - Add an `async` function `main` for the main program logic - Initialize `sessionId` using the command line argument or by calling `getSessionId` - Add a console log statement for the session ID - Remove the `if` statement for checking `sessionId` existence - Update the socket URL to use `API_ENDPOINT` - Add a `socket.on` event listener for the `close` event - Update the condition for checking the `key` value in `stdin.on` event listener - Call the `main` function at the end of the file Signed-off-by: wancat --- server/src/client.js | 71 +++++++++++++++++++++++++++++--------------- 1 file changed, 47 insertions(+), 24 deletions(-) diff --git a/server/src/client.js b/server/src/client.js index 40e16cd..e4bd7f2 100644 --- a/server/src/client.js +++ b/server/src/client.js @@ -1,37 +1,60 @@ +/* eslint-disable no-underscore-dangle */ import { stdin, stdout, exit } from 'process' import { io } from 'socket.io-client' +import { get } from './utils/env.js' + +const API_ENDPOINT = get('API_ENDPOINT', 'http://localhost:3000') stdin.setRawMode(true) stdin.resume() stdin.setEncoding('utf8') -const sessionId = process.argv[2] -console.log(`Session ID: ${sessionId}`) +async function getSessionId() { + const res = await fetch(`${API_ENDPOINT}/api/v1/sessions`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + quest_id: 'helloworld', + }), + }) + const data = await res.json() + return data._id +} -const socket = io('ws://localhost:3000', { - query: { - session_id: sessionId, - }, -}) +async function main() { + const sessionId = process.argv[2] || (await getSessionId()) + console.log(`Session ID: ${sessionId}`) + if (!sessionId) exit() -socket.on('open', function open() { - console.log('Connected to the server.') -}) + const socket = io('ws://localhost:3000', { + query: { + session_id: sessionId, + }, + }) -socket.on('message', function incoming(data) { - stdout.write(data) -}) + socket.on('open', function open() { + console.log('Connected to the server.') + }) -socket.on('close', function close() { - console.log('Disconnected from the server.') - exit() -}) + socket.on('message', function incoming(data) { + stdout.write(data) + }) -stdin.on('data', (key) => { - if (key === '\u0003') { - socket.close() + socket.on('close', function close() { + console.log('Disconnected from the server.') exit() - } else { - socket.send(key) - } -}) + }) + + stdin.on('data', (key) => { + if (key === '\u0004') { + socket.close() + exit() + } else { + socket.send(key) + } + }) +} + +main() From 9376e16f7228ecc903a9a49062f9a06ee6512133 Mon Sep 17 00:00:00 2001 From: wancat Date: Wed, 26 Jul 2023 00:12:10 +0800 Subject: [PATCH 07/11] Success to send command to api + require sourcing entrypoint.sh manually --- cleanup.sh | 5 +++ docker-compose.prod.yml | 32 +++++++++++++++++++ docker-compose.yml | 1 - server/quests/helloworld/Dockerfile | 11 +++++++ server/quests/helloworld/entrypoint.sh | 25 +++++++++++++++ .../src/api/controllers/commandController.js | 1 + server/src/api/socket.js | 1 - 7 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 cleanup.sh create mode 100644 docker-compose.prod.yml create mode 100644 server/quests/helloworld/Dockerfile create mode 100644 server/quests/helloworld/entrypoint.sh diff --git a/cleanup.sh b/cleanup.sh new file mode 100644 index 0000000..09c66ad --- /dev/null +++ b/cleanup.sh @@ -0,0 +1,5 @@ +#!/bin/sh +containers=$(docker ps -a | grep 'quest-' | awk '{print $1}') +echo $containers + +docker rm -f $containers \ No newline at end of file diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..888ecc8 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,32 @@ +version: '3' +services: + app: + image: lancatlin/linux-odyssey + restart: on-failure + build: + context: . + dockerfile: Dockerfile + networks: + - internal + - players + ports: + - '3000:3000' + volumes: + - /var/run/docker.sock:/var/run/docker.sock + environment: + - MONGO_URL=mongodb://db:27017/odyssey-test + - DOCKER_NETWORK=linux-odyssey-players + db: + image: mongo + restart: always + networks: + - internal + ports: + - '27017:27017' + volumes: + - ./data:/data/db + +networks: + internal: + players: + name: linux-odyssey-players diff --git a/docker-compose.yml b/docker-compose.yml index 7cdeb90..8a03ed9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,6 @@ version: '3' services: app: - image: lancatlin/linux-odyssey restart: on-failure build: context: . diff --git a/server/quests/helloworld/Dockerfile b/server/quests/helloworld/Dockerfile new file mode 100644 index 0000000..1f6751b --- /dev/null +++ b/server/quests/helloworld/Dockerfile @@ -0,0 +1,11 @@ +FROM ubuntu:latest + +RUN apt-get update && apt-get install -y \ + curl \ + vim + +WORKDIR /app + +COPY . . + +CMD [ "/bin/bash" ] \ No newline at end of file diff --git a/server/quests/helloworld/entrypoint.sh b/server/quests/helloworld/entrypoint.sh new file mode 100644 index 0000000..f5f4d11 --- /dev/null +++ b/server/quests/helloworld/entrypoint.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# enable programmable completion features (you don't need to enable +# this, if it's already enabled in /etc/bash.bashrc and /etc/profile +# sources /etc/bash.bashrc). +#if [ -f /etc/bash_completion ] && ! shopt -oq posix; then +# . /etc/bash_completion +#fi + +# This function will be called before each command is executed +preexec() { + [ -n "$COMP_LINE" ] && return # do nothing if completing + [ "$BASH_COMMAND" = "$PROMPT_COMMAND" ] && return # do nothing if completing + local command="$BASH_COMMAND" + local pwd="$PWD" + + # Store command in variable + local cmd=$(printf '{"token": "%s", "pwd": "%s", "command": "%s"}' "$TOKEN" "$pwd" "$command") + + # Send command to the API + curl -X POST $API_ENDPOINT/api/v1/commands -H 'accept: */*' -H 'Content-Type: application/json' -d "$cmd" +} + +# Install the trap +trap 'preexec' DEBUG diff --git a/server/src/api/controllers/commandController.js b/server/src/api/controllers/commandController.js index 5f8a887..92c8374 100644 --- a/server/src/api/controllers/commandController.js +++ b/server/src/api/controllers/commandController.js @@ -4,6 +4,7 @@ import Session from '../../models/session.js' import { verifySessionJWT } from '../../utils/auth.js' export async function newCommand(req, res) { + console.log('new command:', req.body) const { token, command, pwd, output } = req.body let sessionId diff --git a/server/src/api/socket.js b/server/src/api/socket.js index 24a75e7..2eea301 100644 --- a/server/src/api/socket.js +++ b/server/src/api/socket.js @@ -54,7 +54,6 @@ export default (server) => { }) socket.on('message', function incoming(message) { - console.log(`From client: ${message}`) stream.write(message) }) From d997a061a91bfd1f216bb1bf0012ab752b098017 Mon Sep 17 00:00:00 2001 From: wancat Date: Wed, 26 Jul 2023 20:53:57 +0800 Subject: [PATCH 08/11] feat: send command typed in container to API - Modify the Dockerfile in the `server/quests/helloworld` directory to add a user and change the working directory - Rename the file `server/quests/helloworld/entrypoint.sh` to `server/quests/helloworld/home/.bashrc` - Modify the `server/src/config.js` file to add the `isProduction` flag - Modify the `server/src/utils/docker.js` file to add a `HostConfig` option and bind a directory if not in production mode - Modify the Dockerfile to add an argument `DOCKER_GID` and create a group and user with the specified `DOCKER_GID` - Add a new Dockerfile.dev file with the same modifications as the Dockerfile - Add a new file `inspect.sh` - Modify the `server/src/client.js` file to use the `API_ENDPOINT` variable instead of a hardcoded URL Signed-off-by: wancat --- Dockerfile | 6 +++++ Dockerfile.dev | 6 +++++ inspect.sh | 2 ++ server/quests/helloworld/Dockerfile | 10 +++++--- .../{entrypoint.sh => home/.bashrc} | 9 -------- server/src/client.js | 2 +- server/src/config.js | 1 + server/src/utils/docker.js | 23 +++++++++++++++---- 8 files changed, 41 insertions(+), 18 deletions(-) create mode 100755 inspect.sh rename server/quests/helloworld/{entrypoint.sh => home/.bashrc} (68%) diff --git a/Dockerfile b/Dockerfile index a3a1f79..464e5ba 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,8 +5,14 @@ COPY package.json yarn.lock ./ COPY server/package.json ./server/ RUN yarn install --frozen-lockfile --production +ARG DOCKER_GID=140 + +RUN groupadd -g $DOCKER_GID docker && usermod -aG docker node + WORKDIR /app/server +USER node + COPY server/src ./src COPY server/quests ./quests COPY server/swagger.yaml . diff --git a/Dockerfile.dev b/Dockerfile.dev index ae536bb..43b79ff 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -7,6 +7,12 @@ RUN yarn install --frozen-lockfile WORKDIR /app/server +ARG DOCKER_GID=140 + +RUN groupadd -g $DOCKER_GID docker && usermod -aG docker node + +USER node + ENV NODE_ENV=development ENV HOST=0.0.0.0 ENV PORT=3000 diff --git a/inspect.sh b/inspect.sh new file mode 100755 index 0000000..7f3c41d --- /dev/null +++ b/inspect.sh @@ -0,0 +1,2 @@ +#!/bin/sh +docker compose logs app -f --no-log-prefix diff --git a/server/quests/helloworld/Dockerfile b/server/quests/helloworld/Dockerfile index 1f6751b..6ef8747 100644 --- a/server/quests/helloworld/Dockerfile +++ b/server/quests/helloworld/Dockerfile @@ -4,8 +4,12 @@ RUN apt-get update && apt-get install -y \ curl \ vim -WORKDIR /app +RUN adduser --disabled-password --gecos '' rudeus -COPY . . +USER rudeus -CMD [ "/bin/bash" ] \ No newline at end of file +WORKDIR /home/rudeus + +COPY ./home /home/rudeus/ + +CMD [ "/bin/bash" ] diff --git a/server/quests/helloworld/entrypoint.sh b/server/quests/helloworld/home/.bashrc similarity index 68% rename from server/quests/helloworld/entrypoint.sh rename to server/quests/helloworld/home/.bashrc index f5f4d11..7c4d2bf 100644 --- a/server/quests/helloworld/entrypoint.sh +++ b/server/quests/helloworld/home/.bashrc @@ -1,12 +1,3 @@ -#!/bin/bash - -# enable programmable completion features (you don't need to enable -# this, if it's already enabled in /etc/bash.bashrc and /etc/profile -# sources /etc/bash.bashrc). -#if [ -f /etc/bash_completion ] && ! shopt -oq posix; then -# . /etc/bash_completion -#fi - # This function will be called before each command is executed preexec() { [ -n "$COMP_LINE" ] && return # do nothing if completing diff --git a/server/src/client.js b/server/src/client.js index e4bd7f2..5e30c93 100644 --- a/server/src/client.js +++ b/server/src/client.js @@ -28,7 +28,7 @@ async function main() { console.log(`Session ID: ${sessionId}`) if (!sessionId) exit() - const socket = io('ws://localhost:3000', { + const socket = io(API_ENDPOINT, { query: { session_id: sessionId, }, diff --git a/server/src/config.js b/server/src/config.js index a228473..6b3abf0 100644 --- a/server/src/config.js +++ b/server/src/config.js @@ -6,4 +6,5 @@ export default { db: get('MONGO_URL', 'mongodb://localhost:27017/odyssey-test'), jwtSecret: get('JWT_SECRET', 'secret'), dockerNetwork: get('DOCKER_NETWORK', 'linux-odyssey-players'), + isProduction: process.env.NODE_ENV === 'production', } diff --git a/server/src/utils/docker.js b/server/src/utils/docker.js index 7f5189b..35869cb 100644 --- a/server/src/utils/docker.js +++ b/server/src/utils/docker.js @@ -3,6 +3,8 @@ import config from '../config.js' const engine = new Docker() +const questPath = '/home/zeko/src/linux-odyssey/server/quests' + const containerOptions = { Image: 'lancatlin/quest-helloworld', AttachStdin: true, @@ -12,23 +14,34 @@ const containerOptions = { Cmd: ['/bin/bash'], OpenStdin: true, StdinOnce: false, + HostConfig: { + Binds: config.isProduction + ? [] + : [`${questPath}/helloworld/home:/home/rudeus`], + }, } const network = engine.getNetwork(config.dockerNetwork) export async function getOrCreateContainer(id) { console.log(`Getting container: ${id}`) + let container try { - const container = engine.getContainer(id) + container = engine.getContainer(id) if (!(await container.inspect()).State.Running) { await container.start() } return container } catch (error) { - const container = await engine.createContainer({ - ...containerOptions, - name: id, - }) + try { + container = await engine.createContainer({ + ...containerOptions, + name: id, + }) + } catch (error) { + console.log(error) + throw error + } await network.connect({ Container: container.id }) await container.start() From b9643e7d5c9d179eb7e44971b9649b686edbeac4 Mon Sep 17 00:00:00 2001 From: wancat Date: Thu, 27 Jul 2023 17:56:32 +0800 Subject: [PATCH 09/11] feat: capture output and exit code of commands + ignore interactive commands like vim, top or nano --- server/quests/helloworld/Dockerfile | 3 ++- server/quests/helloworld/home/.bashrc | 20 ++++++++++++++++--- .../src/api/controllers/commandController.js | 3 ++- server/src/models/command.js | 4 ++++ 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/server/quests/helloworld/Dockerfile b/server/quests/helloworld/Dockerfile index 6ef8747..01f3814 100644 --- a/server/quests/helloworld/Dockerfile +++ b/server/quests/helloworld/Dockerfile @@ -2,7 +2,8 @@ FROM ubuntu:latest RUN apt-get update && apt-get install -y \ curl \ - vim + vim \ + jq RUN adduser --disabled-password --gecos '' rudeus diff --git a/server/quests/helloworld/home/.bashrc b/server/quests/helloworld/home/.bashrc index 7c4d2bf..3a0355f 100644 --- a/server/quests/helloworld/home/.bashrc +++ b/server/quests/helloworld/home/.bashrc @@ -4,12 +4,26 @@ preexec() { [ "$BASH_COMMAND" = "$PROMPT_COMMAND" ] && return # do nothing if completing local command="$BASH_COMMAND" local pwd="$PWD" - + + if [[ $command =~ ^(vim|top|less|more|nano) ]]; then + local payload=$(printf '{"token": "%s", "pwd": "%s", "command": "%s"}' "$TOKEN" "$pwd" "$command") + curl -X POST $API_ENDPOINT/api/v1/commands -H 'accept: */*' -H 'Content-Type: application/json' -d "$payload" + return + fi + + # Execute the command, capture output and exit code + local output=$( { eval $command 2>&1; echo $? > /tmp/exit_code; }) + local exit_code=$(cat /tmp/exit_code) + + local output_esc=$(echo $output | jq -Rs .) + # Store command in variable - local cmd=$(printf '{"token": "%s", "pwd": "%s", "command": "%s"}' "$TOKEN" "$pwd" "$command") + local payload=$(printf '{"token": "%s", "pwd": "%s", "command": "%s", "exit_code": "%s", "output": %s}' "$TOKEN" "$pwd" "$command" "$exit_code" "$output_esc") # Send command to the API - curl -X POST $API_ENDPOINT/api/v1/commands -H 'accept: */*' -H 'Content-Type: application/json' -d "$cmd" + curl -X POST $API_ENDPOINT/api/v1/commands -H 'accept: */*' -H 'Content-Type: application/json' -d "$payload" || echo $payload + + BASH_COMMAND="" } # Install the trap diff --git a/server/src/api/controllers/commandController.js b/server/src/api/controllers/commandController.js index 92c8374..a886d23 100644 --- a/server/src/api/controllers/commandController.js +++ b/server/src/api/controllers/commandController.js @@ -5,7 +5,7 @@ import { verifySessionJWT } from '../../utils/auth.js' export async function newCommand(req, res) { console.log('new command:', req.body) - const { token, command, pwd, output } = req.body + const { token, command, pwd, exit_code, output } = req.body let sessionId try { @@ -27,6 +27,7 @@ export async function newCommand(req, res) { session, command, pwd, + exit_code, output, }) diff --git a/server/src/models/command.js b/server/src/models/command.js index 4ea6697..ce64e7c 100644 --- a/server/src/models/command.js +++ b/server/src/models/command.js @@ -17,6 +17,10 @@ const Command = model( type: String, required: true, }, + exit_code: { + type: Number, + required: false, + }, output: { type: String, required: false, From ec90c484b46737182a6f8150675f4ffada9dcfb3 Mon Sep 17 00:00:00 2001 From: wancat Date: Mon, 31 Jul 2023 15:42:42 +0800 Subject: [PATCH 10/11] feat: add parameters and subcommand to client.js - Added `"commander": "^11.0.0"` as a dependency in the `package.json` file - Added an `import { program } from 'commander'` statement in the `client.js` file - Modified the `createdSession` function in the `client.js` file to use `program.opts().host` instead of `API_ENDPOINT` - Added a `getSessionList` function in the `client.js` file to fetch the list of sessions - Added a `lastSession` function in the `client.js` file to get the last session from the session list - Added a `connect` function in the `client.js` file to establish a socket connection - Modified the `main` function in the `client.js` file to use `program.opts().session` instead of `process.argv[2]` to get the session ID - Modified the `main` function in the `client.js` file to use `program.opts().host` instead of `API_ENDPOINT` to initialize the socket connection - Added a `program` object with options and actions in the `client.js` file - Added `.bash_history` to the `.gitignore` file - Added `.viminfo` to the `.gitignore` file - Added `*.swp` to the `.gitignore` file Signed-off-by: wancat --- .gitignore | 3 ++ server/package.json | 1 + server/src/client.js | 73 ++++++++++++++++++++++++++++++++++++++------ yarn.lock | 5 +++ 4 files changed, 73 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 42d9320..a389d25 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ yarn-debug.log* yarn-error.log* lerna-debug.log* .pnpm-debug.log* +.bash_history +.viminfo +*.swp # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json diff --git a/server/package.json b/server/package.json index b5ec08a..6a40895 100644 --- a/server/package.json +++ b/server/package.json @@ -10,6 +10,7 @@ "test": "yarn node --experimental-vm-modules $(yarn bin jest)" }, "dependencies": { + "commander": "^11.0.0", "dockerode": "^3.3.5", "express": "^4.18.2", "jsonwebtoken": "^9.0.1", diff --git a/server/src/client.js b/server/src/client.js index 5e30c93..557b88d 100644 --- a/server/src/client.js +++ b/server/src/client.js @@ -1,16 +1,16 @@ /* eslint-disable no-underscore-dangle */ import { stdin, stdout, exit } from 'process' import { io } from 'socket.io-client' +import { program } from 'commander' import { get } from './utils/env.js' -const API_ENDPOINT = get('API_ENDPOINT', 'http://localhost:3000') - stdin.setRawMode(true) stdin.resume() stdin.setEncoding('utf8') -async function getSessionId() { - const res = await fetch(`${API_ENDPOINT}/api/v1/sessions`, { +async function createdSession() { + console.log('Creating a new session...') + const res = await fetch(`${program.opts().host}/api/v1/sessions`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -20,15 +20,37 @@ async function getSessionId() { }), }) const data = await res.json() - return data._id + console.log('Created Session ID:', data._id) + return data } -async function main() { - const sessionId = process.argv[2] || (await getSessionId()) +async function getSessionList() { + const res = await fetch(`${program.opts().host}/api/v1/sessions`) + const data = await res.json() + return data +} + +async function lastSession() { + const sessions = await getSessionList() + if (sessions.length === 0) { + console.log( + 'No session found. Please use --create to create a new session.' + ) + exit(1) + } + const session = sessions[sessions.length - 1] + console.log( + `Last Session ID: ${session._id}, Quest: ${session.quest}, Created At: ${session.createdAt}` + ) + return session +} + +async function connect(sessionId) { console.log(`Session ID: ${sessionId}`) + console.log('Use Ctrl + D to exit.') if (!sessionId) exit() - const socket = io(API_ENDPOINT, { + const socket = io(program.opts().host, { query: { session_id: sessionId, }, @@ -57,4 +79,37 @@ async function main() { }) } -main() +async function main() { + console.log(program.opts().session) + // main() + + const sessionId = + program.opts().session || + (program.opts().create ? (await createdSession())._id : null) || + (await lastSession())._id + + await connect(sessionId) +} + +program + .option('-s, --session ', 'Session ID') + .option('-c, --create', 'Create a new session') + .option( + '-h, --host ', + 'Server host', + get('API_ENDPOINT', 'http://localhost:3000') + ) + .action(main) + +program + .command('list') + .description('List all sessions') + .action(async () => { + const sessions = await getSessionList() + sessions.forEach(({ _id, quest, createdAt }) => { + console.log(`${_id} ${quest} ${createdAt}`) + }) + exit() + }) + +program.parse() diff --git a/yarn.lock b/yarn.lock index 3d1cf5e..c888fdb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1486,6 +1486,11 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +commander@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-11.0.0.tgz#43e19c25dbedc8256203538e8d7e9346877a6f67" + integrity sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ== + commander@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" From c3030b22beadccd1a809bbb7df54f262e400e5cd Mon Sep 17 00:00:00 2001 From: wancat Date: Mon, 31 Jul 2023 17:19:03 +0800 Subject: [PATCH 11/11] feat: add mongo init script, use root in app image --- .eslintignore | 3 ++- Dockerfile | 6 ------ docker-compose.prod.yml | 14 ++++++++++++-- mongo-init.js | 10 ++++++++++ server/src/utils/docker.js | 8 ++++---- 5 files changed, 28 insertions(+), 13 deletions(-) create mode 100644 mongo-init.js diff --git a/.eslintignore b/.eslintignore index 55c1e3b..7172e54 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,4 @@ vite.config.js .gitignore -data \ No newline at end of file +data +mongo-init.js \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 464e5ba..a3a1f79 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,14 +5,8 @@ COPY package.json yarn.lock ./ COPY server/package.json ./server/ RUN yarn install --frozen-lockfile --production -ARG DOCKER_GID=140 - -RUN groupadd -g $DOCKER_GID docker && usermod -aG docker node - WORKDIR /app/server -USER node - COPY server/src ./src COPY server/quests ./quests COPY server/swagger.yaml . diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 888ecc8..4644ef6 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -14,7 +14,7 @@ services: volumes: - /var/run/docker.sock:/var/run/docker.sock environment: - - MONGO_URL=mongodb://db:27017/odyssey-test + - MONGO_URL=mongodb://user:user-password@db:27017/odyssey-test - DOCKER_NETWORK=linux-odyssey-players db: image: mongo @@ -24,9 +24,19 @@ services: ports: - '27017:27017' volumes: - - ./data:/data/db + - mongo:/data/db + - ./mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro + environment: + - MONGO_INITDB_ROOT_USERNAME=admin + - MONGO_INITDB_ROOT_PASSWORD=admin-password + - MONGO_USER=user + - MONGO_PASS=user-password + - MONGO_DB=odyssey-test networks: internal: players: name: linux-odyssey-players + +volumes: + mongo: diff --git a/mongo-init.js b/mongo-init.js new file mode 100644 index 0000000..9e4f467 --- /dev/null +++ b/mongo-init.js @@ -0,0 +1,10 @@ +const user = process.env.MONGO_USER +const password = process.env.MONGO_PASS +const dbName = process.env.MONGO_DB + +db = db.getSiblingDB(dbName) +db.createUser({ + user: user, + pwd: password, + roles: [{ role: 'readWrite', db: dbName }], +}) diff --git a/server/src/utils/docker.js b/server/src/utils/docker.js index 35869cb..50fe918 100644 --- a/server/src/utils/docker.js +++ b/server/src/utils/docker.js @@ -6,7 +6,7 @@ const engine = new Docker() const questPath = '/home/zeko/src/linux-odyssey/server/quests' const containerOptions = { - Image: 'lancatlin/quest-helloworld', + Image: 'lancatlin/linux-odyssey:helloworld', AttachStdin: true, AttachStdout: true, AttachStderr: true, @@ -38,9 +38,9 @@ export async function getOrCreateContainer(id) { ...containerOptions, name: id, }) - } catch (error) { - console.log(error) - throw error + } catch (err) { + console.log(err) + throw err } await network.connect({ Container: container.id })