From 021537de56d4609f293de4395ebf7f8ddfd796ef Mon Sep 17 00:00:00 2001 From: Alex Blank Date: Fri, 29 May 2026 17:01:21 +0200 Subject: [PATCH 1/3] fix(cloudflared): replace broken --bind-addr at runtime + new migration Problem: The first migration already ran on the user's server with --bind-addr (broken). Alembic won't re-run the fixed migration. Changes: - _ensure_web_bind_address(): Now detects existing --bind-addr commands and replaces them with --host 0.0.0.0 instead of skipping - New migration 2026_05_29_fix_code_server_bind_addr: Finds code-server tool types with --bind-addr in compose_template and replaces with --host 0.0.0.0 Quality gates: pytest 42 passed (2 pre-existing unrelated failures) --- .../2026_05_29_fix_code_server_bind_addr.py | 53 +++++++++++++++++++ apps/api/src/api/tool_instances.py | 48 +++++++++++------ 2 files changed, 86 insertions(+), 15 deletions(-) create mode 100644 apps/api/alembic/versions/2026_05_29_fix_code_server_bind_addr.py 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..a157b26 --- /dev/null +++ b/apps/api/alembic/versions/2026_05_29_fix_code_server_bind_addr.py @@ -0,0 +1,53 @@ +"""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/src/api/tool_instances.py b/apps/api/src/api/tool_instances.py index 62eabe1..2dd4853 100644 --- a/apps/api/src/api/tool_instances.py +++ b/apps/api/src/api/tool_instances.py @@ -648,28 +648,46 @@ 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: - return + continue # 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( From a7a59058746fafa2d125ba51c1a9a70bc8d164b4 Mon Sep 17 00:00:00 2001 From: Alex Blank Date: Fri, 29 May 2026 17:09:43 +0200 Subject: [PATCH 2/3] fix(cloudflared): remove command override for LSIO images Problem: linuxserver/code-server already binds to 0.0.0.0 by default. Adding any command: override (--bind-addr or --host) breaks the LSIO s6 init system with 'not found' errors. Changes: - _ensure_web_bind_address(): Skip LSIO images entirely (no command override needed). If an existing override is found, remove it. - New migration 2026_05_29_remove_lsio_command_override: Removes --bind-addr and --host command overrides from both DB templates and existing instance compose files on disk for LSIO images. Quality gates: ruff clean --- .../2026_05_29_fix_code_server_bind_addr.py | 9 +- ...2026_05_29_remove_lsio_command_override.py | 109 ++++++++++++++++++ apps/api/src/api/tool_instances.py | 15 +++ 3 files changed, 129 insertions(+), 4 deletions(-) create mode 100644 apps/api/alembic/versions/2026_05_29_remove_lsio_command_override.py 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 index a157b26..cbca2ca 100644 --- 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 @@ -5,6 +5,7 @@ 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 @@ -33,9 +34,7 @@ def upgrade() -> None: 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" - ) + ).replace("--bind-addr", "--host 0.0.0.0") conn.execute( sa.text(""" @@ -46,7 +45,9 @@ def upgrade() -> None: {"compose_template": updated, "id": tool_id}, ) - print(f"Fixed code-server template ({tool_id}): replaced --bind-addr with --host") + print( + f"Fixed code-server template ({tool_id}): replaced --bind-addr with --host" + ) def downgrade() -> None: 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..cb220bf --- /dev/null +++ b/apps/api/alembic/versions/2026_05_29_remove_lsio_command_override.py @@ -0,0 +1,109 @@ +"""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() + + # Find code-server tool types with broken command overrides + 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})") + + # Also clean up existing instance compose files on disk + result = conn.execute( + sa.text(""" + SELECT id, compose_file_path + FROM tool_instances + WHERE compose_file_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 2dd4853..00912b8 100644 --- a/apps/api/src/api/tool_instances.py +++ b/apps/api/src/api/tool_instances.py @@ -652,6 +652,21 @@ def _ensure_web_bind_address(compose_path: str, tool_type_name: str) -> None: 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 is_code_server = tool_type_name == "code-server" and ( "code-server" in image or "coder" in image From eef1e4e8c66fa9a7098788ad673dfd69f0d24538 Mon Sep 17 00:00:00 2001 From: Alex Blank Date: Fri, 29 May 2026 17:17:12 +0200 Subject: [PATCH 3/3] fix(cloudflared): remove command override for LSIO images Problem: linuxserver/code-server already binds to 0.0.0.0 by default. Adding any command: override (--bind-addr or --host) breaks the LSIO s6 init system with 'not found' errors. Changes: - _ensure_web_bind_address(): Skip LSIO images entirely (no command override needed). If an existing override is found, remove it. - New migration 2026_05_29_remove_lsio_command_override: Removes --bind-addr and --host command overrides from both DB templates and existing instance compose files on disk for LSIO images. - Fixed migration to use correct column name (compose_path) and check information_schema for column existence defensively. Quality gates: ruff clean --- .../2026_05_29_fix_code_server_bind_addr.py | 9 +- ...2026_05_29_remove_lsio_command_override.py | 120 ++++++++++++++++++ apps/api/src/api/tool_instances.py | 15 +++ 3 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 apps/api/alembic/versions/2026_05_29_remove_lsio_command_override.py 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 index a157b26..cbca2ca 100644 --- 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 @@ -5,6 +5,7 @@ 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 @@ -33,9 +34,7 @@ def upgrade() -> None: 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" - ) + ).replace("--bind-addr", "--host 0.0.0.0") conn.execute( sa.text(""" @@ -46,7 +45,9 @@ def upgrade() -> None: {"compose_template": updated, "id": tool_id}, ) - print(f"Fixed code-server template ({tool_id}): replaced --bind-addr with --host") + print( + f"Fixed code-server template ({tool_id}): replaced --bind-addr with --host" + ) def downgrade() -> None: 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 2dd4853..00912b8 100644 --- a/apps/api/src/api/tool_instances.py +++ b/apps/api/src/api/tool_instances.py @@ -652,6 +652,21 @@ def _ensure_web_bind_address(compose_path: str, tool_type_name: str) -> None: 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 is_code_server = tool_type_name == "code-server" and ( "code-server" in image or "coder" in image