11from http import HTTPStatus
2+ from types import SimpleNamespace
3+
24from fastapi import Depends , HTTPException
35from sqlalchemy .exc import SQLAlchemyError
46from sqlalchemy .orm import Session
79from app .models .settings import get_settings_dict , Settings
810from app .schemas .settings import SettingsResponse , SettingsUpdateRequest
911from app .tenancy import current_project_id
12+ from app .utils .build_environment import selected_build_environment_provider
13+ from app .utils .build_executor import create_build_executor
14+ from app .utils .build_host import BuildHostConfig
15+ from app .utils .modal_sandbox import ModalSandboxConfig
1016from app .utils .posthog import initialize_posthog
1117from . import api_project
1218
@@ -23,6 +29,13 @@ async def set_settings(data: SettingsUpdateRequest, db: Session = Depends(get_db
2329
2430 try :
2531 current_settings = get_settings_dict (db , project_id = project_id )
32+ merged_settings = {** current_settings , ** payload }
33+ provider = selected_build_environment_provider (merged_settings )
34+ if isinstance (payload .get ("buildHost" ), dict ):
35+ payload ["buildHost" ] = {** payload ["buildHost" ], "enabled" : provider == "buildHost" }
36+ if isinstance (payload .get ("modalSandbox" ), dict ):
37+ payload ["modalSandbox" ] = {** payload ["modalSandbox" ], "enabled" : provider == "modal" }
38+
2639 for key , value in payload .items ():
2740 if value != current_settings .get (key ):
2841 setting = db .query (Settings ).filter_by (project_id = project_id , key = key ).first ()
@@ -39,3 +52,76 @@ async def set_settings(data: SettingsUpdateRequest, db: Session = Depends(get_db
3952 if "posthog" in payload :
4053 initialize_posthog (updated_settings , project_id = project_id )
4154 return updated_settings
55+
56+
57+ @api_project .post ("/settings/test_build_host" )
58+ async def test_build_host (data : SettingsUpdateRequest ):
59+ payload = data .to_dict ()
60+ raw_build_host_settings = payload .get ("buildHost" ) if isinstance (payload , dict ) else None
61+ build_host_config = BuildHostConfig .from_settings (
62+ {** raw_build_host_settings , "enabled" : True } if isinstance (raw_build_host_settings , dict ) else raw_build_host_settings
63+ )
64+ if build_host_config is None :
65+ raise HTTPException (
66+ status_code = HTTPStatus .BAD_REQUEST ,
67+ detail = "Select build host via SSH and enter a host, user, and private SSH key first" ,
68+ )
69+
70+ try :
71+ async with create_build_executor (
72+ build_host_config ,
73+ db = None ,
74+ redis = None ,
75+ frame = SimpleNamespace (id = 0 ),
76+ workspace_prefix = "frameos-build-host-test-" ,
77+ ) as executor :
78+ status , out , err = await executor .run (
79+ "echo frameos-build-host-ok && command -v docker >/dev/null && docker buildx version >/dev/null" ,
80+ log_command = False ,
81+ log_output = False ,
82+ )
83+ except Exception as exc : # noqa: BLE001
84+ raise HTTPException (status_code = HTTPStatus .BAD_GATEWAY , detail = f"Build host connection failed: { exc } " ) from exc
85+
86+ if status != 0 :
87+ raise HTTPException (
88+ status_code = HTTPStatus .BAD_GATEWAY ,
89+ detail = err or out or "Build host is missing Docker or the Docker Buildx plugin" ,
90+ )
91+
92+ return {"ok" : True , "output" : (out or "" ).strip ()}
93+
94+
95+ @api_project .post ("/settings/test_modal_sandbox" )
96+ async def test_modal_sandbox (data : SettingsUpdateRequest ):
97+ payload = data .to_dict ()
98+ raw_modal_settings = payload .get ("modalSandbox" ) if isinstance (payload , dict ) else None
99+ modal_config = ModalSandboxConfig .from_settings (raw_modal_settings )
100+ if modal_config is None :
101+ raise HTTPException (
102+ status_code = HTTPStatus .BAD_REQUEST ,
103+ detail = "Select Modal sandboxes and enter a token ID and token secret first" ,
104+ )
105+
106+ try :
107+ async with create_build_executor (
108+ modal_config ,
109+ db = None ,
110+ redis = None ,
111+ frame = SimpleNamespace (id = 0 ),
112+ ) as executor :
113+ status , out , err = await executor .run (
114+ "command -v nimble && nimble --version >/dev/null && echo frameos-modal-sandbox-ok" ,
115+ log_command = False ,
116+ log_output = False ,
117+ )
118+ except Exception as exc : # noqa: BLE001
119+ raise HTTPException (status_code = HTTPStatus .BAD_GATEWAY , detail = f"Modal sandbox test failed: { exc } " ) from exc
120+
121+ if status != 0 :
122+ raise HTTPException (
123+ status_code = HTTPStatus .BAD_GATEWAY ,
124+ detail = err or out or "Modal sandbox image is missing the FrameOS Nim toolchain" ,
125+ )
126+
127+ return {"ok" : True , "output" : (out or "" ).strip ()}
0 commit comments