mirror of
https://github.com/linux-odyssey/linux-odyssey.git
synced 2026-11-05 19:20:12 +08:00
Doc (#167)
This commit is contained in:
5 files changed
+139
-92
No files matched your search
+38
@@ -0,0 +1,38 @@
|
|||||||
|
HOST=localhost
|
||||||
|
PORT=3000
|
||||||
|
BASE_URL=http://localhost:5173
|
||||||
|
BACKEND_URL=http://localhost:3000
|
||||||
|
MONGO_URL=mongodb://localhost:27017/odyssey-test
|
||||||
|
SECRET_KEY=sample-secret
|
||||||
|
|
||||||
|
# Optional
|
||||||
|
# CONTAINER_EXPIRY=60 # 60 minutes
|
||||||
|
# SESSION_MAX_AGE=168 # 7 days
|
||||||
|
# TRUSTED_PROXIES=1.2.3.4
|
||||||
|
# valid configurations see isIP & isIPRange in https://github.com/validatorjs/validator.js?tab=readme-ov-file#validators
|
||||||
|
|
||||||
|
# GOOGLE_CLIENT_ID=
|
||||||
|
# GOOGLE_CLIENT_SECRET=
|
||||||
|
# GITHUB_CLIENT_ID=
|
||||||
|
# GITHUB_CLIENT_SECRET=
|
||||||
|
# SURVEY_URL=
|
||||||
|
# BUG_REPORT_URL=
|
||||||
|
# DOCKER_NETWORK=
|
||||||
|
|
||||||
|
# SURVEY_URL=
|
||||||
|
# BUG_REPORT_URL=
|
||||||
|
|
||||||
|
# Docker related config
|
||||||
|
# DOCKER_NETWORK=host
|
||||||
|
# QUEST_IMAGE=linuxodyssey/quest-base
|
||||||
|
# DOCKER_PREFIX=linuxodyssey/quest-
|
||||||
|
|
||||||
|
# To mount quest folder from host into container, for development purpose
|
||||||
|
# MOUNT_QUEST=true
|
||||||
|
|
||||||
|
# LOG_PATH=
|
||||||
|
|
||||||
|
# Testing, create a user with the following credentials, disable rate limiting
|
||||||
|
# TESTING=true
|
||||||
|
# TESTING_USERNAME=alex
|
||||||
|
# TESTING_PASSWORD=Alex1234
|
||||||
@@ -5,9 +5,14 @@
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- NodeJS
|
- NodeJS v20+
|
||||||
- Docker
|
- Docker
|
||||||
- Yarn
|
- Yarn: v1.22
|
||||||
|
|
||||||
|
## Known Issues
|
||||||
|
|
||||||
|
- The player's terminal doesn't work correctly on Mac OS and Windows for now. Please use Linux or Virtual Machines. You can track [this issue](https://github.com/linux-odyssey/linux-odyssey/issues/165) for updates.
|
||||||
|
- MongoDB requires x86 architecture to run for v5+ versions. You can track [this issue](https://github.com/linux-odyssey/linux-odyssey/issues/166) for updates.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -15,27 +20,49 @@
|
|||||||
cd linux-odyssey
|
cd linux-odyssey
|
||||||
yarn install
|
yarn install
|
||||||
|
|
||||||
## Development
|
## Project Structures
|
||||||
|
|
||||||
Run mongoDB first:
|
1. **app**: frontend
|
||||||
|
2. **server**: backend
|
||||||
|
3. **quests**: tutorials scripts for each quest
|
||||||
|
4. **packages**: other packages
|
||||||
|
1. **analytics**: admin dashboard to analyze collected data
|
||||||
|
2. **constants**: some shared config values for app and server
|
||||||
|
3. **container**: the script run in container to capture users' command
|
||||||
|
4. **file-graph**: the algorithm to construct and modify file graph, used in both app and server
|
||||||
|
5. **models**: definitions of MondoDB collections (data schema)
|
||||||
|
6. **utils**: other utility code shared between packages
|
||||||
|
7. **game (in the future)**: core logic of game playing, will be extracted from server
|
||||||
|
|
||||||
|
## Docker Setup
|
||||||
|
|
||||||
|
### Docker compose files explaination
|
||||||
|
|
||||||
|
1. **docker-compose.yml**: The one used for development. It sets up MongoDB and builds base quest image.
|
||||||
|
2. **docker-compose.prod.yml**: The one used for production deployment. It has a Nginx service, a backend, a database, an analytics service, and creates isolated networks for player containers.
|
||||||
|
3. **docker-compose.testing.yml**: Similar to production config, but sets up an one-time environment for end-to-end testing.
|
||||||
|
|
||||||
|
### Common Development Setup
|
||||||
|
|
||||||
|
This sets up a development database, and running development server for frontend and backend.
|
||||||
|
|
||||||
|
Pull the latest images:
|
||||||
|
|
||||||
|
docker compose pull
|
||||||
|
|
||||||
|
Start the containers:
|
||||||
|
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
|
|
||||||
The db will automatically restarts by Docker.
|
This will start the db. it will automatically restarts by Docker.
|
||||||
|
|
||||||
Build up dependencies:
|
Building quest images:
|
||||||
|
|
||||||
yarn build:deps
|
yarn build:quests
|
||||||
|
|
||||||
Run dev server (frontend, backend):
|
### Build Quest Images in Details
|
||||||
|
|
||||||
yarn dev
|
In each quest folder, there is a `game.yml` file, which is the quest definition. And a optional `Dockerfile` can overwrite the base image. If you want to include additional files, put them in a `home` folder. The files in `home` will **not** be copied to the image automatically, you have to copy them in the `Dockerfile`.
|
||||||
|
|
||||||
If you want to enable social login, you should have OAuth client token in a `.env` file:
|
|
||||||
|
|
||||||
### Build Quest Images
|
|
||||||
|
|
||||||
In each quest folder, there is a `game.yml` file, which is the quest definition. And a optional `Dockerfile` to overwrite the base image. If you want to include additional files, put them in a `home` folder. The files in `home` will **not** be copied to the image automatically, you have to copy them in the `Dockerfile`.
|
|
||||||
|
|
||||||
quests/
|
quests/
|
||||||
├── Dockerfile # Base image
|
├── Dockerfile # Base image
|
||||||
@@ -60,6 +87,38 @@ To build all quests:
|
|||||||
|
|
||||||
The base image is built with docker-compose, so you can use `docker compose build base` to rebuild it. Once you rebuild the base image, you have to rebuild all quests.
|
The base image is built with docker-compose, so you can use `docker compose build base` to rebuild it. Once you rebuild the base image, you have to rebuild all quests.
|
||||||
|
|
||||||
|
### Mounting Host Directory
|
||||||
|
|
||||||
|
To improve the development experience, we can mount the host directory to the container in the pattern:
|
||||||
|
|
||||||
|
/path/to/linux-odyssey/quests/${quest_id}/home:/home/commander
|
||||||
|
|
||||||
|
You can edit the files on your host, and the changes will be reflected in the container. This is disabled by default. To enable it, you can set the following environment variables:
|
||||||
|
|
||||||
|
MOUNT_QUEST=true
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
Copy `.env.sample` to `.env` and customize it for your needs.
|
||||||
|
|
||||||
|
Build the whole project for the first time:
|
||||||
|
|
||||||
|
yarn build
|
||||||
|
|
||||||
|
This will build the typescript for each package and the server.
|
||||||
|
|
||||||
|
Run dev server (frontend, backend):
|
||||||
|
|
||||||
|
yarn dev
|
||||||
|
|
||||||
|
**IMPORTANT NOTE**: only the app and server have hot-reloading, other packages require manually running `yarn dev` in each package.
|
||||||
|
|
||||||
|
To develop analytics dashboard:
|
||||||
|
|
||||||
|
yarn analytics
|
||||||
|
|
||||||
|
The default username and password is `alex` and `Alex1234`.
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
Run Cypress locally, using the current development containers:
|
Run Cypress locally, using the current development containers:
|
||||||
@@ -78,8 +137,9 @@ Run Cypress in container, create brand-new containers along with it:
|
|||||||
# tearing off the containers
|
# tearing off the containers
|
||||||
./docker-scripts.sh down
|
./docker-scripts.sh down
|
||||||
|
|
||||||
### Deployment
|
## Deployment
|
||||||
|
|
||||||
|
# Build the images (only allowed for maintainers)
|
||||||
docker compose -f docker-compose.prod.yml build
|
docker compose -f docker-compose.prod.yml build
|
||||||
docker compose -f docker-compose.prod.yml push
|
docker compose -f docker-compose.prod.yml push
|
||||||
|
|
||||||
@@ -88,66 +148,4 @@ Run Cypress in container, create brand-new containers along with it:
|
|||||||
docker compose -f docker-compose.prod.yml up -d
|
docker compose -f docker-compose.prod.yml up -d
|
||||||
|
|
||||||
To enable social login, you should have OAuth client token in a `.env` file:
|
To enable social login, you should have OAuth client token in a `.env` file:
|
||||||
You can copy from `example.env`
|
You can copy from `.env.sample`
|
||||||
|
|
||||||
# .env
|
|
||||||
BASE_URL=https://example.com
|
|
||||||
GOOGLE_CLIENT_ID=...
|
|
||||||
GOOGLE_CLIENT_SECRET=...
|
|
||||||
|
|
||||||
To enable it:
|
|
||||||
|
|
||||||
docker compose --env-file .env -f docker-compose.prod.yml up -d
|
|
||||||
|
|
||||||
## Standalone App (without Docker)
|
|
||||||
|
|
||||||
Source: `app/`
|
|
||||||
|
|
||||||
# Connect to dev server (https://odyssey.wancat.cc)
|
|
||||||
yarn app
|
|
||||||
|
|
||||||
# Connect to localhost (http://localhost:3000)
|
|
||||||
yarn app:local
|
|
||||||
|
|
||||||
# Connect to anyhost
|
|
||||||
API_ENDPOINT=http://example.com yarn app:local
|
|
||||||
|
|
||||||
## Swagger
|
|
||||||
|
|
||||||
docker compose up -d swagger
|
|
||||||
|
|
||||||
Open http://localhost:8080 to open Swagger
|
|
||||||
|
|
||||||
## CLI terminal client (Docker not required)
|
|
||||||
|
|
||||||
yarn cli
|
|
||||||
Usage: client [options] [command]
|
|
||||||
|
|
||||||
Options:
|
|
||||||
-s, --session <string> Session ID
|
|
||||||
-c, --create Create a new session
|
|
||||||
-h, --host <string> Server host (default: "http://localhost:3000")
|
|
||||||
--help display help for command
|
|
||||||
|
|
||||||
Commands:
|
|
||||||
list List all sessions
|
|
||||||
|
|
||||||
connet to recent session:
|
|
||||||
|
|
||||||
yarn cli
|
|
||||||
|
|
||||||
connect to new session:
|
|
||||||
|
|
||||||
yarn cli -c
|
|
||||||
|
|
||||||
connect to specific session:
|
|
||||||
|
|
||||||
yarn cli -s 64d0de0367459c13004bc83f
|
|
||||||
|
|
||||||
connect to other host:
|
|
||||||
|
|
||||||
yarn cli -h https://example.com
|
|
||||||
|
|
||||||
list sessions:
|
|
||||||
|
|
||||||
yarn cli list
|
|
||||||
+2
-1
@@ -56,5 +56,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"latest": "^0.2.0",
|
"latest": "^0.2.0",
|
||||||
"typescript-eslint": "^7.4.0"
|
"typescript-eslint": "^7.4.0"
|
||||||
}
|
},
|
||||||
|
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||||
}
|
}
|
||||||
+15
-5
@@ -1,5 +1,7 @@
|
|||||||
import validator from 'validator'
|
import validator from 'validator'
|
||||||
import dotenv from 'dotenv'
|
import dotenv from 'dotenv'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import path from 'path'
|
||||||
import { get } from './utils/env.js'
|
import { get } from './utils/env.js'
|
||||||
|
|
||||||
// Import dotenv and load ../.env
|
// Import dotenv and load ../.env
|
||||||
@@ -43,8 +45,9 @@ function createConfig() {
|
|||||||
db: get('MONGO_URL', 'mongodb://localhost:27017/odyssey-test'),
|
db: get('MONGO_URL', 'mongodb://localhost:27017/odyssey-test'),
|
||||||
secret: get('SECRET_KEY', ''),
|
secret: get('SECRET_KEY', ''),
|
||||||
isProduction: process.env.NODE_ENV === 'production',
|
isProduction: process.env.NODE_ENV === 'production',
|
||||||
containerExpiry: get('EXPIRY', 1000 * 60 * 60),
|
containerExpiry: Number(process.env.CONTAINER_EXPIRY ?? 60) * 1000 * 60,
|
||||||
sessionMaxAge: get('SESSION_MAX_AGE', 1000 * 60 * 60 * 24 * 7),
|
sessionMaxAge:
|
||||||
|
Number(process.env.SESSION_MAX_AGE ?? 7 * 24) * 1000 * 60 * 60,
|
||||||
google: {
|
google: {
|
||||||
clientID: get('GOOGLE_CLIENT_ID', ''),
|
clientID: get('GOOGLE_CLIENT_ID', ''),
|
||||||
clientSecret: get('GOOGLE_CLIENT_SECRET', ''),
|
clientSecret: get('GOOGLE_CLIENT_SECRET', ''),
|
||||||
@@ -61,12 +64,13 @@ function createConfig() {
|
|||||||
network: get('DOCKER_NETWORK', 'host'),
|
network: get('DOCKER_NETWORK', 'host'),
|
||||||
defaultImage: get('QUEST_IMAGE', 'linuxodyssey/quest-base'),
|
defaultImage: get('QUEST_IMAGE', 'linuxodyssey/quest-base'),
|
||||||
imagePrefix: get('DOCKER_PREFIX', 'linuxodyssey/quest-'),
|
imagePrefix: get('DOCKER_PREFIX', 'linuxodyssey/quest-'),
|
||||||
mountQuest: get('MOUNT_QUEST', ''),
|
mountQuest: process.env.MOUNT_QUEST === 'true',
|
||||||
hostPwd: get('HOST_PWD', ''),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
projectRoot: getProjectRoot(),
|
||||||
|
|
||||||
log: {
|
log: {
|
||||||
path: get('LOG_PATH', '../logs'),
|
path: get('LOG_PATH', path.join(getProjectRoot(), 'logs')),
|
||||||
},
|
},
|
||||||
testing: {
|
testing: {
|
||||||
enabled: !isProduction && process.env.TESTING === 'true',
|
enabled: !isProduction && process.env.TESTING === 'true',
|
||||||
@@ -76,6 +80,12 @@ function createConfig() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getProjectRoot(): string {
|
||||||
|
const filename = fileURLToPath(import.meta.url)
|
||||||
|
const root = path.join(path.dirname(filename), '..', '..')
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
const config = createConfig()
|
const config = createConfig()
|
||||||
|
|
||||||
export function getQuestImage(id: string): string {
|
export function getQuestImage(id: string): string {
|
||||||
|
|||||||
@@ -18,19 +18,19 @@ const containerOptions = {
|
|||||||
|
|
||||||
export function createContainer(
|
export function createContainer(
|
||||||
name: string,
|
name: string,
|
||||||
questId: string
|
imageId: string
|
||||||
): Promise<Docker.Container> {
|
): Promise<Docker.Container> {
|
||||||
const option = {
|
const option = {
|
||||||
...containerOptions,
|
...containerOptions,
|
||||||
name,
|
name,
|
||||||
Image: getQuestImage(questId),
|
Image: getQuestImage(imageId),
|
||||||
}
|
}
|
||||||
const { hostPwd, mountQuest } = config.docker
|
|
||||||
if (!config.isProduction && hostPwd && mountQuest === questId) {
|
if (!config.isProduction && config.docker.mountQuest && imageId !== 'base') {
|
||||||
logger.info('Mounting quest folder', mountQuest)
|
logger.info('Mounting quest folder', imageId)
|
||||||
option.HostConfig!.Binds = [
|
option.HostConfig!.Binds = [
|
||||||
`${hostPwd}/quests/${mountQuest}/home:/home/commander`,
|
`${config.projectRoot}/quests/${imageId}/home:/home/commander`,
|
||||||
`${hostPwd}/packages/container:/usr/local/lib/container`,
|
`${config.projectRoot}/packages/container:/usr/local/lib/container`,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
return engine.createContainer(option)
|
return engine.createContainer(option)
|
||||||
|
|||||||
Reference in new issue
Block a user