diff --git a/apps/api/alembic/versions/2026_05_29_fix_code_server_bind_addr.py b/apps/api/alembic/versions/2026_05_29_fix_code_server_bind_addr.py new file mode 100644 index 0000000..cbca2ca --- /dev/null +++ b/apps/api/alembic/versions/2026_05_29_fix_code_server_bind_addr.py @@ -0,0 +1,54 @@ +"""fix code-server bind-addr to host in DB template + +Revision ID: 2026_05_29_fix_code_server_bind_addr +Revises: 2026_05_29_fix_web_tool_bind_address +Create Date: 2026-05-29 15:00:00.000000 + +""" + +from typing import Sequence + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision: str = "2026_05_29_fix_code_server_bind_addr" +down_revision: str | None = "2026_05_29_fix_web_tool_bind_address" +branch_labels: Sequence[str] | None = None +depends_on: Sequence[str] | None = None + + +def upgrade() -> None: + conn = op.get_bind() + + # Find code-server tool types with broken --bind-addr in compose template + result = conn.execute( + sa.text(""" + SELECT id, compose_template + FROM tool_types + WHERE name = 'code-server' + AND compose_template LIKE '%--bind-addr%' + """) + ).fetchall() + + for tool_id, compose_template in result: + updated = compose_template.replace( + "--bind-addr 0.0.0.0:8443", "--host 0.0.0.0" + ).replace("--bind-addr", "--host 0.0.0.0") + + conn.execute( + sa.text(""" + UPDATE tool_types + SET compose_template = :compose_template + WHERE id = :id + """), + {"compose_template": updated, "id": tool_id}, + ) + + print( + f"Fixed code-server template ({tool_id}): replaced --bind-addr with --host" + ) + + +def downgrade() -> None: + pass diff --git a/apps/api/alembic/versions/2026_05_29_remove_lsio_command_override.py b/apps/api/alembic/versions/2026_05_29_remove_lsio_command_override.py new file mode 100644 index 0000000..a951276 --- /dev/null +++ b/apps/api/alembic/versions/2026_05_29_remove_lsio_command_override.py @@ -0,0 +1,120 @@ +"""Remove broken command override from LSIO code-server templates + +Revision ID: 2026_05_29_remove_lsio_command_override +Revises: 2026_05_29_fix_code_server_bind_addr +Create Date: 2026-05-29 15:05:00.000000 + +""" +from collections.abc import Sequence + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision: str = "2026_05_29_remove_lsio_command_override" +down_revision: str | None = "2026_05_29_fix_code_server_bind_addr" +branch_labels: Sequence[str] | None = None +depends_on: Sequence[str] | None = None + + +def upgrade() -> None: + conn = op.get_bind() + + # Fix tool_types templates in DB + result = conn.execute( + sa.text(""" + SELECT id, compose_template + FROM tool_types + WHERE name = 'code-server' + """) + ).fetchall() + + import yaml + from pathlib import Path + + for tool_id, compose_template in result: + try: + data = yaml.safe_load(compose_template) + except Exception: + continue + + if not data or "services" not in data: + continue + + modified = False + for svc in data["services"].values(): + image = svc.get("image", "") + if not image or "linuxserver" not in image: + continue + if "command" in svc: + cmd = svc["command"] + if "--bind-addr" in cmd or "--host" in cmd: + del svc["command"] + modified = True + + if modified: + updated = yaml.dump(data, default_flow_style=False) + conn.execute( + sa.text(""" + UPDATE tool_types + SET compose_template = :compose_template + WHERE id = :id + """), + {"compose_template": updated, "id": tool_id}, + ) + print(f"Removed broken command override from LSIO template ({tool_id})") + + # Fix existing instance compose files on disk + # Use information_schema to check if compose_path column exists + col_result = conn.execute( + sa.text(""" + SELECT column_name + FROM information_schema.columns + WHERE table_name = 'tool_instances' + AND column_name = 'compose_path' + """) + ).fetchone() + + if col_result: + result = conn.execute( + sa.text(""" + SELECT id, compose_path + FROM tool_instances + WHERE compose_path IS NOT NULL + """) + ).fetchall() + + for instance_id, compose_path in result: + path = Path(compose_path) + if not path.exists(): + continue + try: + content = path.read_text() + data = yaml.safe_load(content) + except Exception: + continue + + if not data or "services" not in data: + continue + + modified = False + for svc in data["services"].values(): + image = svc.get("image", "") + if not image or "linuxserver" not in image: + continue + if "command" in svc: + cmd = svc["command"] + if "--bind-addr" in cmd or "--host" in cmd: + del svc["command"] + modified = True + + if modified: + path.write_text(yaml.dump(data, default_flow_style=False)) + print( + f"Removed broken command override from instance compose " + f"({instance_id})" + ) + + +def downgrade() -> None: + pass diff --git a/apps/api/src/api/tool_instances.py b/apps/api/src/api/tool_instances.py index 62eabe1..00912b8 100644 --- a/apps/api/src/api/tool_instances.py +++ b/apps/api/src/api/tool_instances.py @@ -648,28 +648,61 @@ def _ensure_web_bind_address(compose_path: str, tool_type_name: str) -> None: return for service_config in compose_data["services"].values(): - # Skip if command is already overridden - if "command" in service_config: - return - image = service_config.get("image", "") if not image: + continue + + # LSIO images already bind to 0.0.0.0 — command override breaks s6 init + if "linuxserver" in image: + existing_command = service_config.get("command", "") + if "--bind-addr" in existing_command or "--host" in existing_command: + del service_config["command"] + compose_file.write_text( + yaml.dump(compose_data, default_flow_style=False) + ) + logger.warning( + "Removed broken command override from LSIO image: %s", + existing_command, + ) + return return # Check if the image matches a known tool - if tool_type_name == "code-server" and ( + is_code_server = tool_type_name == "code-server" and ( "code-server" in image or "coder" in image - ): - service_config["command"] = bind_command - break - if tool_type_name == "jupyter-notebook" and ( + ) + is_jupyter = tool_type_name == "jupyter-notebook" and ( "jupyter" in image or "notebook" in image - ): - service_config["command"] = bind_command - break + ) + if not is_code_server and not is_jupyter: + continue - compose_file.write_text(yaml.dump(compose_data, default_flow_style=False)) - logger.info("Injected bind address for %s: %s", tool_type_name, bind_command) + existing_command = service_config.get("command", "") + if existing_command: + # Fix broken --bind-addr (replaces with --host) + if "--bind-addr" in existing_command: + service_config["command"] = bind_command + compose_file.write_text( + yaml.dump(compose_data, default_flow_style=False) + ) + logger.warning( + "Replaced broken bind address for %s: %s → %s", + tool_type_name, + existing_command, + bind_command, + ) + return + # Already has correct --host, nothing to do + if "--host" in existing_command or "--ip=" in existing_command: + return + # Some other command override exists — don't touch it + return + + # No command yet — inject the correct bind address + service_config["command"] = bind_command + compose_file.write_text(yaml.dump(compose_data, default_flow_style=False)) + logger.info("Injected bind address for %s: %s", tool_type_name, bind_command) + return @router.post(