commit 673f096f25f8d1290411ac62e35893ef87a8855e Author: guochao Date: Thu Feb 13 00:04:07 2025 +0800 demo nsenter approach to execute command in another container diff --git a/fastapi-nsenter/.dockerignore b/fastapi-nsenter/.dockerignore new file mode 100644 index 0000000..0aa05c1 --- /dev/null +++ b/fastapi-nsenter/.dockerignore @@ -0,0 +1,3 @@ +.venv +__pycache__ +data \ No newline at end of file diff --git a/fastapi-nsenter/.gitignore b/fastapi-nsenter/.gitignore new file mode 100644 index 0000000..0aa05c1 --- /dev/null +++ b/fastapi-nsenter/.gitignore @@ -0,0 +1,3 @@ +.venv +__pycache__ +data \ No newline at end of file diff --git a/fastapi-nsenter/Dockerfile b/fastapi-nsenter/Dockerfile new file mode 100644 index 0000000..3e2b8b1 --- /dev/null +++ b/fastapi-nsenter/Dockerfile @@ -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 \ No newline at end of file diff --git a/fastapi-nsenter/compose.yml b/fastapi-nsenter/compose.yml new file mode 100644 index 0000000..a6c89a2 --- /dev/null +++ b/fastapi-nsenter/compose.yml @@ -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: \ No newline at end of file diff --git a/fastapi-nsenter/entrypoint.sh b/fastapi-nsenter/entrypoint.sh new file mode 100755 index 0000000..5a26138 --- /dev/null +++ b/fastapi-nsenter/entrypoint.sh @@ -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 \ No newline at end of file diff --git a/fastapi-nsenter/main.py b/fastapi-nsenter/main.py new file mode 100644 index 0000000..d878704 --- /dev/null +++ b/fastapi-nsenter/main.py @@ -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]) + + diff --git a/fastapi-nsenter/requirements.txt b/fastapi-nsenter/requirements.txt new file mode 100644 index 0000000..a25cd25 --- /dev/null +++ b/fastapi-nsenter/requirements.txt @@ -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 \ No newline at end of file diff --git a/fastapi-nsenter/static/index.html b/fastapi-nsenter/static/index.html new file mode 100644 index 0000000..c86c814 --- /dev/null +++ b/fastapi-nsenter/static/index.html @@ -0,0 +1,19 @@ + + + + + convert to pdf + + + +
+ + + +
+ + \ No newline at end of file diff --git a/simple-docker/compose.yaml b/simple-docker/compose.yaml new file mode 100644 index 0000000..219b41d --- /dev/null +++ b/simple-docker/compose.yaml @@ -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: