demo nsenter approach to execute command in another container

This commit is contained in:
guochao 2025-02-13 00:04:07 +08:00
commit 673f096f25
9 changed files with 176 additions and 0 deletions

View File

@ -0,0 +1,3 @@
.venv
__pycache__
data

3
fastapi-nsenter/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.venv
__pycache__
data

View File

@ -0,0 +1,21 @@
FROM alpine:3.21 AS base
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && apk upgrade
FROM base AS soffice
RUN apk add --no-cache libreoffice musl-locales font-wqy-zenhei
VOLUME /data
CMD /bin/sh -c "echo \$\$ > /data/libreoffice.pid; while true; do sleep 1000000; done"
FROM base AS venv-builder
RUN apk add python3-dev uv gcc musl-dev linux-headers
WORKDIR /app
COPY requirements.txt .
RUN uv venv -i https://mirrors.aliyun.com/pypi/simple && uv pip install -i https://mirrors.aliyun.com/pypi/simple -r requirements.txt
FROM base AS service
RUN apk add python3
COPY --from=venv-builder /app/.venv /app/.venv
COPY ./ /app/
WORKDIR /app
VOLUME /data
CMD /app/entrypoint.sh

View File

@ -0,0 +1,24 @@
services:
libreoffice:
build:
target: soffice
restart: always
volumes:
- data:/data
container_name: libreoffice
service:
build: .
restart: always
depends_on:
- libreoffice
volumes:
- data:/data
environment:
- DATA_DIR=/data
pid: container:libreoffice
cap_add:
- CAP_SYS_ADMIN
ports:
- 8000:8000
volumes:
data:

8
fastapi-nsenter/entrypoint.sh Executable file
View File

@ -0,0 +1,8 @@
#!/usr/bin/env sh
set -x
NSENTER_PID=$(cat /data/libreoffice.pid)
export UNO_COMMAND="nsenter -t ${NSENTER_PID} -m soffice --convert-to {outputfmt} --outdir {outputdir} {inputfilename}"
. .venv/bin/activate
exec fastapi run

59
fastapi-nsenter/main.py Normal file
View File

@ -0,0 +1,59 @@
from fastapi import FastAPI, staticfiles, UploadFile
from starlette.responses import FileResponse
import aiofile
from asyncio import subprocess
import tempfile
import os
import glob
import enum
class OutputFormat(enum.Enum):
pdf = "pdf"
data_dir = os.path.realpath(os.environ.get("DATA_DIR", "./data"))
soffice_data_dir = os.path.realpath(os.environ.get("UNO_DATA_DIR", data_dir))
os.makedirs(data_dir, exist_ok=True)
soffice_command = os.environ.get("UNO_COMMAND", "soffice --convert-to {outputfmt} --outdir {outputdir} {inputfilename}")
app = FastAPI()
app.mount("/static", staticfiles.StaticFiles(directory="static"), name="static")
@app.get("/")
async def get_index():
return FileResponse('static/index.html')
@app.post("/")
async def convert_file(file: UploadFile, output_format: OutputFormat | None = None):
output_format = output_format or OutputFormat.pdf
outpath = tempfile.mkdtemp(dir=data_dir)
inputfile = os.path.join(outpath, file.filename)
uno_path = os.path.join(soffice_data_dir, os.path.relpath(outpath, data_dir))
uno_inputfile = os.path.join(uno_path, file.filename)
async with aiofile.AIOFile(inputfile, mode="wb+") as fp:
try:
while True:
content = await file.read(1024*1024*4)
if not content:
break
await fp.write(content)
except EOFError:
pass
else:
print(f"file written to {inputfile}")
command = soffice_command.format(outputfmt=output_format.value, outputdir=uno_path, inputfilename=uno_inputfile)
result_process = await subprocess.create_subprocess_shell(command)
assert await result_process.wait() == 0, f"expect return code 0, got {result_process.returncode}"
outfiles = glob.glob(f"{uno_path}/*.{output_format.value}")
assert len(outfiles) == 1, f"expect one output file, got {outfiles}"
return FileResponse(outfiles[0])

View File

@ -0,0 +1,5 @@
fastapi[standard]==0.115.8
starlette==0.45.3
uvicorn==0.34.0
python-multipart==0.0.20
aiofile==3.9.0

View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>convert to pdf</title>
</head>
<body>
<form method="post" enctype="multipart/form-data">
<label for="file">choose file<input type="file" name="file" id="file" accept=".pptx,.docx"></label>
<label for="file">Output type
<select name="output_format">
<option value="pdf">PDF</option>
</select>
</label>
<button type="submit">submit</button>
</form>
</body>
</html>

View File

@ -0,0 +1,34 @@
services:
sometools:
# 对于 docker compose 来说,没法指定容器来共享 pid需要固定容器名称来给下面的另一个容器使用
container_name: sometools
# demo 用的这里刻意区分了debian版本
image: debian:buster
# 核心是 echo $$ > /shared-pid/tool-container.pid, $$ 是自身 PID
# echo 以后就闲置了,开始昏睡
# 四个$是因为yaml中两个$表示一个$,我们需要$$
command: bash -c 'set -x; echo $$$$ > /shared-pid/tool-container.pid; while true; do sleep 100000; done'
volumes:
# 这个卷两个容器共享
- shared-pid:/shared-pid
do-something-here:
image: debian:bookworm
# 核心是 nsenter进入到另一个容器的命名空间去执行命令
# 应该会看到另一个容器的系统版本
command: bash -c 'set -x; nsenter -t $(cat /shared-pid/tool-container.pid) -m cat /etc/os-release; echo 'exec into this container and run nsenter'; while true; do sleep 10000; done'
volumes:
# 这个卷两个容器共享
- shared-pid:/shared-pid
# 共用上面容器的 PID 命名空间
pid: container:sometools
# 在上面这个容器启动后再启动,因为需要等待新建 PID 命名空间
depends_on:
- sometools
# 对于有一些情况,需要特权
cap_add:
- CAP_SYS_ADMIN
# 实在不行就开特权
# privileged: true
volumes:
shared-pid: