from fastapi import FastAPI, status from pydantic import BaseModel, Field from pathlib import Path from datetime import datetime, timedelta import docker app = FastAPI() client = docker.from_env() class PyPistonServer(BaseModel): server_id: str | None = None server_name: str | None = None container_name: str image: str = Field( title="The base docker image that was used to create this server" ) version: str = Field( title="The version of the docker image that was used to create this server" ) description: str | None = Field( default=None, title="Description or notes for the server instance", max_length=300 ) motd: str | None = Field( default=None, title="Message of the Day that will appear on the Minecraft server listing page in the minecraft client", max_length=300 ) rcon_enabled: bool = Field( default=True, title="Whether or not this container can be managed via rcon" ) ports: dict = Field( default={25565: 25565}, title="List of ports that have been published to the host" ) is_vanilla: bool = Field( default=False, title="Whether or not the server is running a modded vanilla (non-modded/custom) of Minecraft" ) snooper_enabled: bool = True status: str | None = Field( default=None, title="State of the docker container" ) uptime: str | None = Field( default=None, title="Time the container has been up" ) world_name: str = "world" online_mode: bool = True allow_flight: bool = False def get_env_value(env_key: str, env_list: list): val = [e for e in env_list if f"{env_key}=" in e] return val[0].split('=')[-1] if val else None def get_servers(): for container in client.containers.list(filters={"ancestor": "itzg/minecraft-server"}, all=True): image = container.image.tags[0] env_list = container.attrs['Config']['Env'] yield PyPistonServer( server_id=container.id, server_name=get_env_value("SERVER_NAME", env_list), container_name=container.name, image=image, version=get_env_value("VERSION", env_list) or "Vanilla", description="", motd=get_env_value("MOTD", env_list), rcon_enabled=get_env_value("ENABLE_RCON", env_list) != "FALSE", ports={k: v[0]['HostPort'] for k, v in container.ports.items() if v}, is_vanilla=get_env_value("TYPE", env_list) is None, snooper_enabled=get_env_value("SNOOPER_ENABLED", env_list) == "TRUE", status=([s for s in container.attrs['State'] if container.attrs['State'][s] is True] or ["Starting"])[0], uptime=str(datetime.now().astimezone() - datetime.fromisoformat(container.attrs['Created'])), world_name=get_env_value("LEVEL", env_list) or "world", online_mode=get_env_value("ONLINE_MODE", env_list) != "FALSE", allow_flight=get_env_value("ALLOW_FLIGHT", env_list) == "TRUE" ) @app.get("/", status_code=status.HTTP_200_OK) def read_root(): return {"version": "0.1-dev"} @app.get("/servers/") def read_active_servers(): mc_servers = list(get_servers()) return { "total": len(mc_servers), "servers": mc_servers } @app.post("/servers/new") def create_new_server(server: PyPistonServer): env = { "SERVER_NAME": server.server_name, "MOTD": server.motd, "ENABLE_RCON": server.rcon_enabled, "SNOOPER_ENABLED": server.snooper_enabled, "LEVEL": server.world_name, "ONLINE_MODE": server.online_mode, "ALLOW_FLIGHT": server.allow_flight } if server.version != "Vanilla": env["VERSION"] = server.version new_srv = client.containers.run( server.image, ports=server.ports, restart_policy={"Name": "on-failure", "MaximumRetryCount": 5}, labels={"pyPistonManged": "True"}, environment=env, detach=True ) new_srv = [s for s in get_servers() if s.server_id == new_srv.id][0] return new_srv @app.get("/servers/{server_id}") def get_server_by_id(server_id: str): return [s for s in get_servers() if s.server_id == server_id][0] @app.post("/servers/{server_id}/exec") def run_cmd_on_server(server_id: str, command: str): server = [s for s in get_servers() if s.server_id == server_id][0] container = client.containers.get(server.server_id) if server.rcon_enabled: cmd = f"rcon-cli {command}" else: cmd = f"mc-send-to-console {command}" result = container.exec_run(cmd) return { "exit_code": result.exit_code, "output": result.output.decode("utf-8") }