Skip to content

Commit

Permalink
Merge branch 'staging' into solomon/add-create-test
Browse files Browse the repository at this point in the history
  • Loading branch information
tituschewxj committed Nov 13, 2024
2 parents c236012 + bb8cc46 commit 780aa3d
Show file tree
Hide file tree
Showing 65 changed files with 1,568 additions and 291 deletions.
52 changes: 40 additions & 12 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2

- name: Set up .env
env:
QUESTION_FIREBASE_CREDENTIAL_PATH: ${{ vars.QUESTION_SERVICE_FIREBASE_CREDENTIAL_PATH }}
Expand All @@ -30,7 +30,7 @@ jobs:
echo "FIREBASE_CREDENTIAL_PATH=$QUESTION_FIREBASE_CREDENTIAL_PATH" >> .env
echo "JWT_SECRET=$JWT_SECRET" >> .env
echo "EXECUTION_SERVICE_URL=$EXECUTION_SERVICE_URL" >> .env
- name: Set up credentials
env:
QUESTION_FIREBASE_JSON: ${{ secrets.QUESTION_SERVICE_FIREBASE_CREDENTIAL }}
Expand All @@ -41,8 +41,8 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: "1.23.x"
with:
go-version: '1.23.x'

- name: Install Go dependencies
run: |
Expand All @@ -51,7 +51,7 @@ jobs:
- name: Install firebase tools
run: curl -sL firebase.tools | bash

- name: Run Go tests with Firebase emulator
run: firebase emulators:exec --only firestore 'cd ./apps/question-service; go test -v ./tests'

Expand All @@ -66,11 +66,11 @@ jobs:
run: |
cd ./apps/frontend
cp .env.example .env
- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: "22"
node-version: '22'

- name: Install pnpm
run: npm i -g pnpm
Expand All @@ -83,7 +83,7 @@ jobs:
- name: Run tests
run: |
cd ./apps/frontend
pnpm test
pnpm unit-test
test-docker-compose:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -148,13 +148,13 @@ jobs:
cd ../history-service
echo "FIREBASE_CREDENTIAL_PATH=$HISTORY_FIREBASE_CREDENTIAL_PATH" >> .env
echo "PORT=$HISTORY_SERVICE_PORT" >> .env
echo "RABBITMQ_URL=$RABBITMQ_URL" >> .env
echo "RABBMITMQ_URL=$RABBITMQ_URL" >> .env
cd ../execution-service
echo "FIREBASE_CREDENTIAL_PATH=$EXECUTION_FIREBASE_CREDENTIAL_PATH" >> .env
echo "PORT=$EXECUTION_SERVICE_PORT" >> .env
echo "HISTORY_SERVICE_URL=$HISTORY_SERVICE_URL" >> .env
echo "RABBITMQ_URL=$RABBITMQ_URL" >> .env
echo "RABBMITMQ_URL=$RABBITMQ_URL" >> .env
cd ../signalling-service
echo "PORT=$SIGNALLING_SERVICE_PORT" >> .env
Expand All @@ -173,7 +173,7 @@ jobs:
cd ../history-service
echo "$HISTORY_FIREBASE_JSON" > "./$HISTORY_FIREBASE_CREDENTIAL_PATH"
cd ../execution-service
echo "$EXECUTION_FIREBASE_JSON" > "./$EXECUTION_FIREBASE_CREDENTIAL_PATH"
Expand Down Expand Up @@ -201,6 +201,7 @@ jobs:
SIGNALLING_SERVICE_URL: ${{ vars.SIGNALLING_SERVICE_URL }}
EXECUTION_SERVICE_URL: ${{ vars.EXECUTION_SERVICE_URL }}
run: |
docker ps -a
echo "Testing Question Service..."
curl -sSL -o /dev/null $QUESTION_SERVICE_URL && echo "Question Service is up"
echo "Testing User Service..."
Expand All @@ -225,3 +226,30 @@ jobs:
echo "WebSocket for Signalling Service is live"
fi
# We can add more tests here
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9.1.4

- name: Install dependencies
run: |
cd ./apps/frontend
pnpm i
- name: Install Chrome WebDriver
uses: nanasess/setup-chromedriver@v2
with:
chromedriver-version: '130.0.6723.116'
- name: Install Edge
uses: browser-actions/setup-edge@v1
with:
edge-version: stable

- name: Install Geckodriver
uses: browser-actions/setup-geckodriver@latest

- name: Run Browser Test
run: |
cd ./apps/frontend
pnpm browser-test
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,38 @@
- You can choose to develop individual microservices within separate folders within this repository **OR** use individual repositories (all public) for each microservice.
- In the latter scenario, you should enable sub-modules on this GitHub classroom repository to manage the development/deployment **AND** add your mentor to the individual repositories as a collaborator.
- The teaching team should be given access to the repositories as we may require viewing the history of the repository in case of any disputes or disagreements.

---

## Architecture Diagram

![Overall Architecture Diagram](./docs/architecture_diagram.png)

The overall architecture of PeerPrep follows a microservices architecture. The client acts as an orchestrator for the interaction between the different services.

## Screenshots

![Home Page](./docs/home_page.png)

![Collaboration Page](./docs/collab_page_1.png)

![Collaboration Page](./docs/collab_page_2.png)

![Question Page](./docs/question_page.png)

![Question Page](./docs/indiv_question_page.png)

![History Page](./docs/submission_history_page.png)

## More details

- [Frontend](./apps/frontend/README.md)
- [User Service](./apps/user-service/README.md)
- [Question Service](./apps/question-service/README.md)
- [Matching Service](./apps/matching-service/README.md)
- [Signalling Service](./apps/signalling-service/README.md)
- [History Service](./apps/history-service/README.md)
- [Execution Service](./apps/execution-service/README.md)
- [CI/CD Guide](./docs/cicid.md)
- [Docker Compose Guide](./apps/README.md)
- [Set Up Guide](./docs/setup.md)
37 changes: 31 additions & 6 deletions apps/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

This project uses Docker Compose to manage multiple services such as a frontend, backend, and a database. The configuration is defined in the `docker-compose.yml` file, and environment variables can be stored in environment files for different environments (e.g., development, production).

More details on how to set up Docker Compose can be found [here](../docs/setup.md)

## Prerequisites

Before you begin, ensure you have the following installed on your machine:
Expand Down Expand Up @@ -30,11 +32,25 @@ In the `./apps` directory:
├── user-service
│ ├── Dockerfile # Dockerfile for user-service
│ └── ... (other user-service files)
├── execution-service
│ ├── Dockerfile # Dockerfile for execution-service
│ └── ... (other execution-service files)
├── signalling-service
│ ├── Dockerfile # Dockerfile for signalling-service
│ └── ... (other signalling-service files)
├── history-service
│ ├── Dockerfile # Dockerfile for history-service
│ └── ... (other history-service files)
```

## Docker Compose Setup

Ensure that you are currently using **Docker Compose v2** in your local Docker Desktop.
- Launch your local Docker Desktop application
- Click on settings button at the top right hand corner (beside the name)
- Under the General tab, scroll down until you see a checkbox that says Use Docker Compose V2, ensure that the box is checked then apply and restart (refer to the image below)
![Docker Compose V2](https://github.com/user-attachments/assets/3b8d47c2-c488-4fc1-804d-418ffebbdd9c)

By using multiple Dockerfiles in Docker Compose, we can manage complex multi-container applications where each service has its own environment and build process.

1. Build and Start the Application
Expand All @@ -54,11 +70,15 @@ This will:

Once running, you can access:

- The **frontend** at http://localhost:3000
- The **user service** at http://localhost:3001
- The **question service** at http://localhost:8080 (REST) and http://localhost:50051 (gRPC)
- The **matching service** at http://localhost:8081
- The **redis service** at http://localhost:6379
- The [**frontend**](./frontend/README.md) at http://localhost:3000
- The [**user-service**](./user-service/README.md) at http://localhost:3001
- The [**question-service**](./question-service/README.md) at http://localhost:8080 (REST) and http://localhost:50051 (gRPC)
- The [**matching-service**](./matching-service/README.md) at http://localhost:8081
- The [**history-service**](./history-service/README.md) at http://localhost:8082
- The [**execution-service**](./execution-service/README.md) at http://localhost:8083
- The [**signalling-service**](./signalling-service/README.md) at http://localhost:4444
- The **redis** at http://localhost:6379
- The **rabbitmq** at http://localhost:5672

3. Stopping Services

Expand All @@ -76,6 +96,11 @@ This command will stop and remove the containers, networks, and volumes created

- **Port Conflicts**: If you encounter port conflicts, ensure the host ports specified in docker-compose.yml (e.g., 3000:3000) are not in use by other applications.
- **Environment Variables Not Loaded**: Ensure the `.env` files are in the correct directories as found in the `docker-compose.yml` file.
- **Command execution failed**: When you try running test cases or submitting the code in the collaborative environment, if you encounter the following error message:
```bash
Command execution failed: Unable to find image 'apps-python-sandbox:latest' locally docker: Error response from daemon: pull access denied for apps-python-sandbox, repository does not exist or may require 'docker login': denied: requested access to the resource is denied. See 'docker run --help'. : exit status 125
```
Ensure that you have **Docker Compose V2** enabled for your Docker Desktop application. Please refer to the Docker Compose setup guide above to enable it locally.

### Known Issues

Expand Down
9 changes: 9 additions & 0 deletions apps/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,15 @@ services:
- apps_network
container_name: python-sandbox

node-sandbox:
build:
context: ./execution-service/execution/node
dockerfile: Dockerfile
networks:
- apps_network
container_name: node-sandbox
stdin_open: true # Enables interactive mode for passing standard input

networks:
apps_network:

Expand Down
62 changes: 58 additions & 4 deletions apps/execution-service/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,59 @@
# Execution Service

The Execution Service provides backend functionality for running and validating code executions or submissions within a coding platform. It enables users to execute code against test cases and receive feedback on the correctness of their solutions.

The Execution Service incorporates a code execution mechanism designed to run user-submitted solutions within an isolated, sandboxed environment. This approach enhances security by preventing arbitrary code from interacting with the host system directly and allows for performance monitoring

### Technology Stack

- Golang (Go): Statically typed, compiled language with low latency. Fast and efficient processing is ideal for high-read, high-write environments like in Execution Service, when many users run tests or submit tests.
- Rest Server: chi router was utilized which supports CORS, logging and timeout via middlewares. It is stateless, which reduces coupling and enhances scalability and reliability, simplicity and flexibility. For example, clients may make requests to different server instances when scaled.
- Firebase Firestore: NoSQL Document database that is designed for automatic horizontal scaling and schema-less design that allows for flexibility as number of tests increases or more users run tests.
- Docker: used to containerize the Execution Service to simplify deployment. Additionally used to provide a sandboxed execution environment for user-submitted code, ensuring security by limiting code access to the host system and managing dependencies independently.

### Execution Process

For execution of user code (running of test cases without submission), only visible (public) and custom test cases are executed.

![Diagram of code execution process](../../docs/exeuction_process.png)

### Submission Process

For submission of user code, both visible (public) and hidden testcases are executed, before calling the history-service API to submit the submission data, code and test results.

![Diagram of code submission process](../../docs/submission_process.png)

### Design Decisions

1. **Docker Containerisation**
a. Upon receiving a code execution request, the service dynamically creates a Docker container with a controlled environment tailored to Python
b. The Docker container is set up with only the minimal permissions and resources needed to execute the code, restricting the execution environment to reduce risk
c. This containerized environment is automatically destroyed after execution, ensuring no residual data or state remains between executions

2. **Security and Isolation**
a. Containers provide isolation from the host system, limiting any interaction between user code and the underlying infrastructure
b. Only essential files and libraries required for code execution are included, reducing potential attack surfaces within each container. The sandboxed, container-based execution system provides a secure and efficient way to run user code submissions.

The sandboxed, container-based execution system provides a secure and efficient way to run user code submissions.

### Communication between Execution and History Service

The communication between the Execution service and the History service is implemented through a RabbitMQ Message Queue. RabbitMQ is ideal for message queues in microservices due to its reliability, flexible routing, and scalability. It ensures messages aren’t lost through durable queues and supports complex routing to handle diverse messaging needs.

Asynchronous communication was chosen as a user’s submission history did not need to be updated immediately. Instead of waiting for a response, the Execution Service can put the message in a queue and continue processing other requests.

![RabbitMQ Message Queue](./../../docs/rabbit_mq_queue.png)

A message queue allows services to communicate without depending on each other's availability. The Execution Service can send a message to the queue, and the History Service can process it when it’s ready. This decoupling promotes loose coupling and reduces dependencies between services, which helps maintain a robust and adaptable system.

---

## Setup

### Prerequisites

Ensure you have Go installed on your machine.

### Installation

1. Install dependencies:
Expand Down Expand Up @@ -61,10 +115,10 @@ The server will be available at http://localhost:8083.

## API Endpoints

- `POST /tests/populate`
- `GET /tests/{questionDocRefId}/`
- `POST /tests/{questionDocRefId}/execute`
- `POST /tests/{questionDocRefId}/submit`
- `POST: /tests/populate`: Deletes and repopulates all tests in Firebase
- `GET: /{questionDocRefId}`: Reads the public testcases for the question, identified by the question reference ID
- `POST: /{questionDocRefId}/execute`: Executes the public testcases for the question, identified by the question reference ID
- `POST: /{questionDocRefId}/submit`: Executes the public and hidden testcases for the question, identified by the question reference ID, and submits the code submission to History Service

## Managing Firebase

Expand Down
12 changes: 6 additions & 6 deletions apps/execution-service/constants/constant.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package constants

const (
JAVA = "Java"
PYTHON = "Python"
GOLANG = "Golang"
JAVASCRIPT = "Javascript"
CPP = "C++"
JAVA = "java"
PYTHON = "python"
GOLANG = "golang"
JAVASCRIPT = "javascript"
CPP = "c++"
)

const (
Expand All @@ -17,6 +17,6 @@ var IS_VALID_LANGUAGE = map[string]bool{
PYTHON: true,
//JAVA: true,
//GOLANG: true,
//JAVASCRIPT: true,
JAVASCRIPT: true,
//CPP: true,
}
11 changes: 11 additions & 0 deletions apps/execution-service/execution/node/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Use a slim Node.js image
FROM node:18-slim

# Set the working directory
WORKDIR /app

# Install any dependencies if necessary (you can skip if no dependencies)
# COPY package*.json ./
# RUN npm install

# No entry point or CMD needed as you'll provide the command at runtime
33 changes: 33 additions & 0 deletions apps/execution-service/execution/node/javascript.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package node

import (
"bytes"
"fmt"
"os/exec"
"strings"
)

func RunJavaScriptCode(code string, input string) (string, string, error) {
cmd := exec.Command(
"docker", "run", "--rm",
"-i", // allows standard input to be passed in
"apps-node-sandbox", // Docker image with Node.js environment
"node", "-e", code, // Runs JavaScript code with Node.js
)

// Pass input to the JavaScript script
cmd.Stdin = bytes.NewBufferString(input)

// Capture standard output and error output
var output bytes.Buffer
var errorOutput bytes.Buffer
cmd.Stdout = &output
cmd.Stderr = &errorOutput

// Run the command
if err := cmd.Run(); err != nil {
return "", fmt.Sprintf("Command execution failed: %s: %v", errorOutput.String(), err), nil
}

return strings.TrimSuffix(output.String(), "\n"), strings.TrimSuffix(errorOutput.String(), "\n"), nil
}
Loading

0 comments on commit 780aa3d

Please sign in to comment.