-
Notifications
You must be signed in to change notification settings - Fork 2
feat:Added bind mount support to allow for persistent storage #65
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,21 @@ | ||
| #!/bin/sh | ||
| while true; do | ||
| raw=$(cat pipe) | ||
|
|
||
| if echo "$raw" | grep -q "^RESPOND::"; then | ||
| cmd="${raw##RESPOND::}" | ||
| response_file=$(mktemp) | ||
| eval "$cmd" > "$response_file" 2>&1 | ||
| ( | ||
| while ! mkdir output_pipe.lock 2>/dev/null; do | ||
| sleep 1 | ||
| done | ||
|
|
||
| while true; do eval "$(cat pipe)"; done | ||
| cat "$response_file" > output_pipe | ||
| rm -f "$response_file" | ||
| rmdir output_pipe.lock | ||
| ) & | ||
| else | ||
| eval "$raw" | ||
| fi | ||
| done |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,9 +5,15 @@ name=$2 | |
| resource=$3 | ||
| exp_port=$4 | ||
| max_mem=$5 | ||
| enable_volume=$6 | ||
|
|
||
| available_ports=() | ||
|
|
||
| STORAGE_ROOT="/mnt/storage" | ||
| PROJECT_STORAGE="$STORAGE_ROOT/$name" | ||
| PROJECT_IMG="$STORAGE_ROOT/$name.img" | ||
| SIZE_MB=100 | ||
| sudo mkdir -p "$STORAGE_ROOT" | ||
| sudo chmod 755 "$STORAGE_ROOT" | ||
| for ((port=PORT_MIN; port<=PORT_MAX; port++)); do | ||
| if ! ss -ln src :$port | grep -q "\<$port\>"; then | ||
| available_ports+=($port) | ||
|
|
@@ -17,9 +23,9 @@ done | |
| echo "Available ports: ${available_ports[56]}" | ||
| AVAILABLE=0 | ||
| echo "Creating subdomain $name" | ||
| git clone $resource $name | ||
| sudo cp .env $name/ | ||
| cd $name | ||
| git clone $resource "$name" | ||
| sudo cp .env "$name/" | ||
| cd "$name" | ||
|
|
||
| if [ $flag = "-g" ]; then | ||
| sudo cp ../Dockerfile ./ | ||
|
|
@@ -30,19 +36,47 @@ elif [ $flag = "-s" ]; then | |
| COPY . /usr/share/nginx/html | ||
| " > Dockerfile | ||
| fi | ||
| if [ "$enable_volume" = "true" ]; then | ||
| echo "Creating persistent storage at $PROJECT_STORAGE" | ||
|
|
||
| if [ ! -f "$PROJECT_IMG" ]; then | ||
| sudo dd if=/dev/zero of="$PROJECT_IMG" bs=1M count=$SIZE_MB | ||
| sudo mkfs.ext4 "$PROJECT_IMG" | ||
| fi | ||
| sudo mkdir -p "$PROJECT_STORAGE" | ||
| if ! mountpoint -q "$PROJECT_STORAGE"; then | ||
| sudo mount -o loop "$PROJECT_IMG" "$PROJECT_STORAGE" | ||
| fi | ||
| sudo chmod 777 "$PROJECT_STORAGE" | ||
| fi | ||
| sudo docker build -t $name . | ||
|
|
||
| # Safety net: If the frontend sends double requests from spam-clicking, forcefully remove any zombie container holding the name | ||
| sudo docker rm -f $name 2>/dev/null || true | ||
|
|
||
| sudo docker run --memory=$max_mem --name=$name -d -p ${available_ports[$AVAILABLE]}:$exp_port $name | ||
| if [ "$enable_volume" = "true" ]; then | ||
| sudo docker run \ | ||
| --memory=$max_mem \ | ||
| --name="$name" \ | ||
| -d \ | ||
| -p ${available_ports[$AVAILABLE]}:$exp_port \ | ||
| -v "$PROJECT_STORAGE":/app/data \ | ||
| -e DATA_DIR=/app/data \ | ||
|
Comment on lines
+39
to
+64
|
||
| "$name" | ||
| else | ||
| sudo docker run \ | ||
| --memory=$max_mem \ | ||
| --name="$name" \ | ||
| -d \ | ||
| -p ${available_ports[$AVAILABLE]}:$exp_port \ | ||
| "$name" | ||
| fi | ||
| cd .. | ||
| sudo rm -rf $name | ||
| sudo rm -rf "$name" | ||
| sudo rm Dockerfile | ||
| sudo rm .env | ||
| sudo touch /etc/nginx/sites-available/$name.conf | ||
| sudo chmod 666 /etc/nginx/sites-available/$name.conf | ||
| sudo touch "/etc/nginx/sites-available/$name".conf | ||
| sudo chmod 666 "/etc/nginx/sites-available/$name.conf" | ||
| sudo echo "# Virtual Host configuration for $name | ||
| server { | ||
| listen 80; | ||
|
|
@@ -58,6 +92,6 @@ sudo echo "# Virtual Host configuration for $name | |
| } | ||
| charset utf-8; | ||
| client_max_body_size 20M; | ||
| }" > /etc/nginx/sites-available/$name.conf | ||
| sudo ln -s /etc/nginx/sites-available/$name.conf /etc/nginx/sites-enabled/$name.conf | ||
| }" > "/etc/nginx/sites-available/$name.conf" | ||
| sudo ln -s "/etc/nginx/sites-available/$name.conf" "/etc/nginx/sites-enabled/$name.conf" | ||
| sudo systemctl reload nginx | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| import { exec } from "../dependencies.ts"; | ||
|
|
||
| const STORAGE_TIMEOUT_MS = 5000; | ||
|
|
||
| let storagePipeLock: Promise<void> = Promise.resolve(); | ||
| async function withStoragePipeLock<T>(operation: () => Promise<T>): Promise<T> { | ||
| const previousLock = storagePipeLock; | ||
| let releaseLock!: () => void; | ||
| storagePipeLock = new Promise<void>((resolve) => { | ||
| releaseLock = resolve; | ||
| }); | ||
| await previousLock; | ||
| try { | ||
| return await operation(); | ||
| } finally { | ||
| releaseLock(); | ||
| } | ||
| } | ||
| export async function canAllocateStorage(requestedMb: number) { | ||
| const STORAGE_PATH = "/mnt/storage"; | ||
| const SAFETY_BUFFER_MB = 200; // keep buffer for system + docker | ||
|
|
||
| try { | ||
| return await withStoragePipeLock(async () => { | ||
| const abortController = new AbortController(); | ||
| const timeoutId = setTimeout(() => abortController.abort(), STORAGE_TIMEOUT_MS); | ||
| try { | ||
| const responseProcess = new Deno.Command("sh", { | ||
| args: ["-c", "cat /hostpipe/output_pipe"], | ||
| }).output(); // don't await yet, just start it | ||
| await exec(`bash -c "echo 'RESPOND::df ${STORAGE_PATH} --output=avail' > /hostpipe/pipe"`); | ||
| const response = await responseProcess; | ||
| const output = new TextDecoder().decode(response.stdout).trim().split("\n"); | ||
| const availableKb = parseInt(output[1].trim()); | ||
| if (isNaN(availableKb)) { | ||
| throw new Error(`Unexpected df output: ${output}`); | ||
| } | ||
| const availableMb = Math.floor(availableKb / 1024); | ||
| const usableMb = availableMb - SAFETY_BUFFER_MB; | ||
| const canAllocate = usableMb >= requestedMb; | ||
| console.log(`can allocate ${canAllocate} memory`); | ||
| console.log(`Available memory is ${availableMb} requested is ${requestedMb}`); | ||
| return { | ||
| can_allocate: canAllocate, | ||
| available_mb: usableMb, | ||
| requested_mb: requestedMb, | ||
| reason: canAllocate ? null : "Not enough disk space", | ||
| }; | ||
| } finally { | ||
| clearTimeout(timeoutId); | ||
| } | ||
| }); | ||
|
|
||
| } catch (err) { | ||
| console.log(`Error during memory check volume`); | ||
| return { | ||
| can_allocate: false, | ||
| available_mb: 0, | ||
| requested_mb: requestedMb, | ||
| reason: "Failed to check disk space", | ||
| }; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,15 +26,20 @@ const domain = import.meta.env.VITE_APP_DOMAIN | |
| <div v-if="static_content === 'No'" class="stack-section"> | ||
| <div class="docker-content"> | ||
| <label for="dockerfile-content">Do you have dockerfile in your repo ?</label><br> | ||
| <input name="radio" type="radio" value="Yes" v-model="dockerfile_present"> Yes | ||
| <input name="radio" type="radio" value="No" v-model="dockerfile_present"> No | ||
| <input name="docker_radio" type="radio" value="Yes" v-model="dockerfile_present"> Yes | ||
| <input name="docker_radio" type="radio" value="No" v-model="dockerfile_present"> No | ||
| </div> | ||
| <div v-if="dockerfile_present === 'No'" class="dockerfile-section"> | ||
| <p>Stack:</p> | ||
| <select class="dropdown" v-model="stack"> | ||
| <option v-for="option in stacks" :key="option">{{ option }}</option> | ||
| </select> | ||
| </div> | ||
| <div class="volume-needed"> | ||
| <label for="volume">Do you need persistent storage (Volume)?</label><br> | ||
| <input name="volume_radio" type="radio" value="Yes" v-model="volume_needed"> Yes | ||
| <input name="volume_radio" type="radio" value="No" v-model="volume_needed"> No | ||
| </div> | ||
|
Comment on lines
+38
to
+42
|
||
| <p>Port:<br><input class="input-field" v-model="port" /></p> | ||
| <div v-if="dockerfile_present === 'No'" class="dockerfile-section"> | ||
| <p>Build Commands:<br><textarea class="textarea-field" cols="50" rows="10" v-model="build_cmds"></textarea></p> | ||
|
|
@@ -67,6 +72,7 @@ export default { | |
| env_content: 'key1 = value1', // Default prompt text | ||
| static_content: 'No', | ||
| dockerfile_present :'No', | ||
| volume_needed: 'No', | ||
| port: '', | ||
| stack: '', | ||
| build_cmds: '', | ||
|
|
@@ -78,14 +84,15 @@ export default { | |
| methods: { | ||
| submitForm() { | ||
| console.log(this.subdomain, this.resource_type, this.resource); | ||
| create(this.subdomain, this.resource_type, this.resource, this.env_content, this.static_content,this.dockerfile_present,this.port, this.stack, this.build_cmds, this.enable_ci) | ||
| create(this.subdomain, this.resource_type, this.resource, this.env_content, this.static_content,this.dockerfile_present, this.volume_needed, this.port, this.stack, this.build_cmds,this.enable_ci) | ||
| .then((res) => { | ||
| console.log(res); | ||
| if (res === 'Submitted') { | ||
| this.closeModalAndReload(); | ||
| } else { | ||
| this.closeModal(); | ||
| alert('Failed to create subdomain'); | ||
| if(res=="insufficient_storage")alert("Insufficient storage to mount a volume"); | ||
| else alert('Failed to create subdomain'); | ||
| setTimeout(() => { | ||
| window.location.reload(); | ||
| }, 1000); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The label passed to
shellEscapeshould describe the field being escaped. Using"false"here will produce confusing error messages (e.g. "Invalid characters in false"); pass a label like"volume_needed"instead.