Files
menzeljandClaude Opus 4.8 f732cb080b Initial commit: StackPilot Phase 1 (Core)
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>
2026-06-07 16:04:58 +00:00

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)