Self-hosted Docker Compose manager. - Backend: FastAPI + docker-py + SQLite (JWT auth, file-first stacks, lifecycle, live status, WebSocket logs, docker-run converter, audit log) - Frontend: React + Vite + Tailwind (login/setup, dashboard, stacks, stack detail, Monaco editor, dark/light theme) - Deployment: docker-compose.yml, Dockerfiles, nginx reverse proxy Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
136 lines
4.2 KiB
Python
136 lines
4.2 KiB
Python
"""Convert a `docker run ...` command string into a Compose YAML fragment."""
|
|
from __future__ import annotations
|
|
|
|
import shlex
|
|
|
|
import yaml
|
|
|
|
|
|
def _service_name_from_image(image: str) -> str:
|
|
name = image.split("/")[-1].split(":")[0]
|
|
return name or "app"
|
|
|
|
|
|
def convert_docker_run(command: str) -> str:
|
|
tokens = shlex.split(command)
|
|
# Drop leading "docker run" / "docker container run".
|
|
while tokens and tokens[0] in ("docker", "run", "container", "sudo"):
|
|
tokens.pop(0)
|
|
|
|
service: dict = {}
|
|
name = None
|
|
image = None
|
|
ports: list[str] = []
|
|
volumes: list[str] = []
|
|
environment: list[str] = []
|
|
env_file: list[str] = []
|
|
devices: list[str] = []
|
|
cap_add: list[str] = []
|
|
labels: list[str] = []
|
|
networks: list[str] = []
|
|
command_args: list[str] = []
|
|
|
|
i = 0
|
|
n = len(tokens)
|
|
|
|
def take_value(idx: int, tok: str):
|
|
if "=" in tok and tok.startswith("--") and not tok.endswith("="):
|
|
return tok.split("=", 1)[1], idx + 1
|
|
return tokens[idx + 1], idx + 2
|
|
|
|
while i < n:
|
|
tok = tokens[i]
|
|
if image is not None:
|
|
# Everything after the image is the container command.
|
|
command_args = tokens[i:]
|
|
break
|
|
if not tok.startswith("-"):
|
|
image = tok
|
|
i += 1
|
|
continue
|
|
|
|
flag = tok.split("=", 1)[0]
|
|
if flag in ("-d", "--detach", "-i", "--interactive", "-t", "--tty", "--rm", "--init"):
|
|
i += 1
|
|
elif flag in ("--name",):
|
|
name, i = take_value(i, tok)
|
|
elif flag in ("-p", "--publish"):
|
|
val, i = take_value(i, tok)
|
|
ports.append(val)
|
|
elif flag in ("-v", "--volume", "--mount"):
|
|
val, i = take_value(i, tok)
|
|
volumes.append(val)
|
|
elif flag in ("-e", "--env"):
|
|
val, i = take_value(i, tok)
|
|
environment.append(val)
|
|
elif flag in ("--env-file",):
|
|
val, i = take_value(i, tok)
|
|
env_file.append(val)
|
|
elif flag in ("--device",):
|
|
val, i = take_value(i, tok)
|
|
devices.append(val)
|
|
elif flag in ("--cap-add",):
|
|
val, i = take_value(i, tok)
|
|
cap_add.append(val)
|
|
elif flag in ("-l", "--label"):
|
|
val, i = take_value(i, tok)
|
|
labels.append(val)
|
|
elif flag in ("--network", "--net"):
|
|
val, i = take_value(i, tok)
|
|
networks.append(val)
|
|
elif flag in ("--restart",):
|
|
val, i = take_value(i, tok)
|
|
service["restart"] = val
|
|
elif flag in ("--privileged",):
|
|
service["privileged"] = True
|
|
i += 1
|
|
elif flag in ("--hostname", "-h"):
|
|
val, i = take_value(i, tok)
|
|
service["hostname"] = val
|
|
elif flag in ("-u", "--user"):
|
|
val, i = take_value(i, tok)
|
|
service["user"] = val
|
|
elif flag in ("-w", "--workdir"):
|
|
val, i = take_value(i, tok)
|
|
service["working_dir"] = val
|
|
else:
|
|
# Unknown flag — try to consume a value if it looks like it takes one.
|
|
if "=" in tok:
|
|
i += 1
|
|
elif i + 1 < n and not tokens[i + 1].startswith("-"):
|
|
i += 2
|
|
else:
|
|
i += 1
|
|
|
|
if not image:
|
|
raise ValueError("Could not find an image in the docker run command")
|
|
|
|
service["image"] = image
|
|
if "restart" not in service:
|
|
service["restart"] = "unless-stopped"
|
|
if ports:
|
|
service["ports"] = ports
|
|
if volumes:
|
|
service["volumes"] = volumes
|
|
if environment:
|
|
service["environment"] = environment
|
|
if env_file:
|
|
service["env_file"] = env_file
|
|
if devices:
|
|
service["devices"] = devices
|
|
if cap_add:
|
|
service["cap_add"] = cap_add
|
|
if labels:
|
|
service["labels"] = labels
|
|
if networks:
|
|
service["networks"] = networks
|
|
if command_args:
|
|
service["command"] = command_args
|
|
|
|
svc_name = name or _service_name_from_image(image)
|
|
if name:
|
|
service["container_name"] = name
|
|
|
|
doc = {"services": {svc_name: service}}
|
|
return yaml.safe_dump(doc, sort_keys=False, default_flow_style=False)
|