Skip to content

Commit 4b40d7d

Browse files
devyubinCopilot
andauthored
feat: add TLS and wildcard domain options to TUI installer (#7638)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 995253a commit 4b40d7d

File tree

5 files changed

+199
-21
lines changed

5 files changed

+199
-21
lines changed

changes/7638.feature.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add `--public-mode`, `--fqdn-prefix`, `--tls-advertised`, and `--advertised-port` options to the TUI installer for public-facing deployments with wildcard domain support.

src/ai/backend/install/app.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,12 @@ def __init__(
414414
self.install_variable = InstallVariable(
415415
public_facing_address=args.public_facing_address,
416416
accelerator=Accelerator(args.accelerator) if args.accelerator is not None else None,
417+
fqdn_prefix=args.fqdn_prefix,
418+
tls_advertised=args.tls_advertised,
419+
advertised_port=args.advertised_port,
420+
endpoint_protocol=args.endpoint_protocol,
421+
frontend_mode=args.frontend_mode,
422+
use_wildcard_binding=args.use_wildcard_binding,
417423
)
418424

419425
def compose(self) -> ComposeResult:

src/ai/backend/install/cli.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import click
88

99
from . import __version__
10-
from .types import Accelerator, CliArgs, InstallModes
10+
from .types import Accelerator, CliArgs, EndpointProtocol, FrontendMode, InstallModes
1111

1212

1313
@click.command(
@@ -58,6 +58,42 @@
5858
default="127.0.0.1",
5959
help="Set public facing address for the Backend.AI server.",
6060
)
61+
@click.option(
62+
"--fqdn-prefix",
63+
type=str,
64+
default=None,
65+
help="FQDN prefix for generating domain names (e.g., '786cdf' generates 786cdf.app.backend.ai, 786cdf.apphub.backend.ai, etc.).",
66+
)
67+
@click.option(
68+
"--tls-advertised",
69+
is_flag=True,
70+
default=False,
71+
help="Advertise TLS endpoints to external clients.",
72+
)
73+
@click.option(
74+
"--advertised-port",
75+
type=int,
76+
default=443,
77+
help="Advertised port for public endpoints (default: 443).",
78+
)
79+
@click.option(
80+
"--endpoint-protocol",
81+
type=click.Choice([p.value for p in EndpointProtocol], case_sensitive=False),
82+
default=None,
83+
help="Force endpoint protocol in webserver (http or https). If not set, auto-detected.",
84+
)
85+
@click.option(
86+
"--frontend-mode",
87+
type=click.Choice([m.value for m in FrontendMode], case_sensitive=False),
88+
default=FrontendMode.PORT.value,
89+
help="App-proxy frontend mode: 'port' (default) or 'wildcard'.",
90+
)
91+
@click.option(
92+
"--use-wildcard-binding",
93+
is_flag=True,
94+
default=False,
95+
help="Use wildcard domain binding for app-proxy worker.",
96+
)
6197
@click.version_option(version=__version__)
6298
@click.pass_context
6399
def main(
@@ -68,6 +104,12 @@ def main(
68104
non_interactive: bool,
69105
headless: bool,
70106
public_facing_address: str,
107+
fqdn_prefix: str | None,
108+
tls_advertised: bool,
109+
advertised_port: int,
110+
endpoint_protocol: str | None,
111+
frontend_mode: str,
112+
use_wildcard_binding: bool,
71113
accelerator: str,
72114
) -> None:
73115
"""The installer"""
@@ -91,6 +133,12 @@ def main(
91133
non_interactive=non_interactive,
92134
public_facing_address=public_facing_address,
93135
accelerator=accelerator,
136+
fqdn_prefix=fqdn_prefix,
137+
tls_advertised=tls_advertised,
138+
advertised_port=advertised_port,
139+
endpoint_protocol=EndpointProtocol(endpoint_protocol) if endpoint_protocol else None,
140+
frontend_mode=FrontendMode(frontend_mode),
141+
use_wildcard_binding=use_wildcard_binding,
94142
)
95143
app = InstallerApp(args)
96144
app.run(headless=headless)

src/ai/backend/install/context.py

Lines changed: 97 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
from .types import (
5353
Accelerator,
5454
DistInfo,
55+
FrontendMode,
5556
HalfstackConfig,
5657
HostPortPair,
5758
ImageSource,
@@ -601,14 +602,30 @@ async def configure_webserver(self) -> None:
601602
conf_path = self.copy_config("webserver.conf")
602603
halfstack = self.install_info.halfstack_config
603604
service = self.install_info.service_config
605+
endpoint_protocol = self.install_variable.endpoint_protocol
606+
fqdn_prefix = self.install_variable.fqdn_prefix
607+
storage_public_address = self.install_variable.storage_public_address
608+
public_facing_address = self.install_variable.public_facing_address
604609
assert halfstack.redis_addr is not None
610+
611+
# use FQDN if provided, otherwise use public_facing_address
612+
if fqdn_prefix is not None:
613+
# With FQDN prefix, use public storage address with https
614+
wsproxy_url = f"https://{storage_public_address}:5050"
615+
else:
616+
# Without FQDN prefix, use public_facing_address with http
617+
wsproxy_url = f"http://{public_facing_address}:5050"
618+
# Use sed_in_place for dotted key wsproxy.url
619+
self.sed_in_place(
620+
conf_path,
621+
re.compile(r'^wsproxy\.url\s*=\s*".*"', flags=re.MULTILINE),
622+
f'wsproxy.url = "{wsproxy_url}"',
623+
)
624+
605625
with conf_path.open("r") as fp:
606626
data = tomlkit.load(fp)
607-
appproxy_itable = tomlkit.inline_table()
608-
appproxy_itable["url"] = (
609-
f"http://{service.appproxy_coordinator_addr.face.host}:{service.appproxy_coordinator_addr.face.port}"
610-
)
611-
data["service"]["appproxy"] = appproxy_itable # type: ignore
627+
if endpoint_protocol is not None:
628+
data["service"]["force_endpoint_protocol"] = endpoint_protocol.value # type: ignore
612629
data["api"][ # type: ignore
613630
"endpoint"
614631
] = f"http://{service.manager_addr.face.host}:{service.manager_addr.face.port}"
@@ -737,6 +754,14 @@ async def configure_appproxy(self) -> None:
737754
self.log.write(f"DB PORT = {halfstack.postgres_addr.face.port}")
738755
self.log.write(f"API SECRET = {service.appproxy_api_secret}")
739756

757+
tls_advertised = self.install_variable.tls_advertised
758+
advertised_port = self.install_variable.advertised_port
759+
wildcard_domain = self.install_variable.wildcard_domain
760+
public_facing_address = self.install_variable.public_facing_address
761+
apphub_address = self.install_variable.apphub_address
762+
app_address = self.install_variable.app_address
763+
frontend_mode = self.install_variable.frontend_mode
764+
740765
with coord_conf.open("r") as fp:
741766
data = tomlkit.load(fp)
742767
data["db"]["type"] = "postgresql" # type: ignore[index]
@@ -747,38 +772,90 @@ async def configure_appproxy(self) -> None:
747772
data["db"]["max_overflow"] = 64 # type: ignore[index]
748773
data["db"]["addr"]["host"] = halfstack.postgres_addr.face.host # type: ignore[index]
749774
data["db"]["addr"]["port"] = halfstack.postgres_addr.face.port # type: ignore[index]
750-
data["redis"]["host"] = halfstack.redis_addr.face.host # type: ignore
751-
data["redis"]["port"] = halfstack.redis_addr.face.port # type: ignore
775+
redis_addr_table = tomlkit.inline_table()
776+
redis_addr_table["host"] = halfstack.redis_addr.face.host # type: ignore
777+
redis_addr_table["port"] = halfstack.redis_addr.face.port # type: ignore
778+
data["redis"]["addr"] = redis_addr_table # type: ignore
752779
data["secrets"]["api_secret"] = service.appproxy_api_secret # type: ignore[index]
753780
data["secrets"]["jwt_secret"] = service.appproxy_jwt_secret # type: ignore[index]
754-
data["permit_hash"]["permit_hash_secret"] = service.appproxy_permit_hash_secret # type: ignore[index]
755-
data["proxy_coordinator"]["bind_addr"]["host"] = ( # type: ignore[index]
756-
service.appproxy_coordinator_addr.bind.host
757-
)
781+
data["permit_hash"]["secret"] = service.appproxy_permit_hash_secret # type: ignore[index]
782+
data["proxy_coordinator"]["bind_addr"]["host"] = "0.0.0.0" # type: ignore[index]
758783
data["proxy_coordinator"]["bind_addr"]["port"] = ( # type: ignore[index]
759784
service.appproxy_coordinator_addr.bind.port
760785
)
786+
data["proxy_coordinator"]["advertised_addr"]["host"] = apphub_address # type: ignore[index]
787+
data["proxy_coordinator"]["advertised_addr"]["port"] = ( # type: ignore[index]
788+
service.appproxy_coordinator_addr.bind.port
789+
)
790+
if tls_advertised:
791+
data["proxy_coordinator"]["tls_advertised"] = True # type: ignore[index]
792+
data["proxy_coordinator"]["advertised_addr"]["port"] = advertised_port # type: ignore[index]
761793
with coord_conf.open("w") as fp:
762794
tomlkit.dump(data, fp)
763795

764796
# Worker
765797
worker_conf = self.copy_config("app-proxy-worker.toml")
766798
with worker_conf.open("r") as fp:
767799
data = tomlkit.load(fp)
768-
data["redis"]["host"] = halfstack.redis_addr.face.host # type: ignore
769-
data["redis"]["port"] = halfstack.redis_addr.face.port # type: ignore
800+
# Update redis addr inline table
801+
redis_addr_table = tomlkit.inline_table()
802+
redis_addr_table["host"] = halfstack.redis_addr.face.host # type: ignore
803+
redis_addr_table["port"] = halfstack.redis_addr.face.port # type: ignore
804+
data["redis"]["addr"] = redis_addr_table # type: ignore
805+
770806
data["proxy_worker"]["coordinator_endpoint"] = ( # type: ignore[index]
771807
f"http://{service.appproxy_coordinator_addr.bind.host}:{service.appproxy_coordinator_addr.bind.port}"
772808
)
773-
data["proxy_worker"]["api_bind_addr"] = { # type: ignore[index]
774-
"host": service.appproxy_worker_addr.bind.host,
775-
"port": service.appproxy_worker_addr.bind.port,
776-
}
777-
data["proxy_worker"]["port_proxy"]["bind_port"] = service.appproxy_worker_addr.bind.port # type: ignore[index]
778-
data["proxy_worker"]["port_proxy"]["bind_host"] = service.appproxy_worker_addr.bind.host # type: ignore[index]
809+
810+
# api_bind_addr as inline table
811+
api_bind_addr_table = tomlkit.inline_table()
812+
api_bind_addr_table["host"] = service.appproxy_worker_addr.bind.host
813+
api_bind_addr_table["port"] = service.appproxy_worker_addr.bind.port
814+
data["proxy_worker"]["api_bind_addr"] = api_bind_addr_table # type: ignore[index]
815+
816+
# api_advertised_addr as inline table
817+
api_advertised_addr_table = tomlkit.inline_table()
818+
api_advertised_addr_table["host"] = public_facing_address
819+
api_advertised_addr_table["port"] = service.appproxy_worker_addr.bind.port
820+
data["proxy_worker"]["api_advertised_addr"] = api_advertised_addr_table # type: ignore[index]
821+
779822
data["secrets"]["api_secret"] = service.appproxy_api_secret # type: ignore[index]
780823
data["secrets"]["jwt_secret"] = service.appproxy_jwt_secret # type: ignore[index]
781-
data["permit_hash"]["permit_hash_secret"] = service.appproxy_permit_hash_secret # type: ignore[index]
824+
data["permit_hash"]["secret"] = service.appproxy_permit_hash_secret # type: ignore[index]
825+
826+
# advertise TLS to external clients
827+
if tls_advertised:
828+
data["proxy_worker"]["tls_advertised"] = True # type: ignore[index]
829+
830+
# set frontend mode (port or wildcard)
831+
data["proxy_worker"]["frontend_mode"] = frontend_mode.value # type: ignore[index]
832+
833+
# configure based on frontend_mode
834+
if frontend_mode == FrontendMode.WILDCARD:
835+
# Remove port_proxy section for wildcard mode
836+
if "port_proxy" in data["proxy_worker"]: # type: ignore[operator]
837+
del data["proxy_worker"]["port_proxy"] # type: ignore[union-attr]
838+
839+
# Override api_advertised_addr with app_address and advertised_port
840+
api_advertised_addr_table = tomlkit.inline_table()
841+
api_advertised_addr_table["host"] = app_address
842+
api_advertised_addr_table["port"] = advertised_port
843+
data["proxy_worker"]["api_advertised_addr"] = api_advertised_addr_table # type: ignore[index]
844+
845+
# Add wildcard_domain section
846+
if wildcard_domain:
847+
wildcard_table = tomlkit.table()
848+
wildcard_table["domain"] = wildcard_domain
849+
bind_addr_table = tomlkit.inline_table()
850+
bind_addr_table["host"] = "0.0.0.0"
851+
bind_addr_table["port"] = 10250
852+
wildcard_table["bind_addr"] = bind_addr_table
853+
wildcard_table["advertised_port"] = advertised_port
854+
wildcard_table.add(tomlkit.nl()) # Add newline before next section
855+
data["proxy_worker"]["wildcard_domain"] = wildcard_table # type: ignore[index]
856+
else:
857+
# update port_proxy.advertised_host
858+
data["proxy_worker"]["port_proxy"]["advertised_host"] = public_facing_address # type: ignore[index]
782859
with worker_conf.open("w") as fp:
783860
tomlkit.dump(data, fp)
784861

src/ai/backend/install/types.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,16 @@ class Platform(enum.StrEnum):
4646
MACOS_X86_64 = "macos-x86_64"
4747

4848

49+
class FrontendMode(enum.StrEnum):
50+
PORT = "port"
51+
WILDCARD = "wildcard"
52+
53+
54+
class EndpointProtocol(enum.StrEnum):
55+
HTTP = "http"
56+
HTTPS = "https"
57+
58+
4959
@dataclasses.dataclass()
5060
class CliArgs:
5161
mode: InstallModes | None
@@ -54,6 +64,12 @@ class CliArgs:
5464
non_interactive: bool
5565
public_facing_address: str
5666
accelerator: Optional[str] = None
67+
fqdn_prefix: Optional[str] = None
68+
tls_advertised: bool = False
69+
advertised_port: int = 443
70+
endpoint_protocol: EndpointProtocol | None = None
71+
frontend_mode: FrontendMode = FrontendMode.PORT
72+
use_wildcard_binding: bool = False
5773

5874

5975
class PrerequisiteError(RichCast, Exception):
@@ -185,3 +201,33 @@ class ServiceConfig:
185201
class InstallVariable:
186202
public_facing_address: str = "127.0.0.1"
187203
accelerator: Optional[Accelerator] = None
204+
fqdn_prefix: Optional[str] = None
205+
tls_advertised: bool = False
206+
advertised_port: int = 443
207+
endpoint_protocol: EndpointProtocol | None = None
208+
frontend_mode: FrontendMode = FrontendMode.PORT
209+
use_wildcard_binding: bool = False
210+
211+
@property
212+
def apphub_address(self) -> str:
213+
if self.fqdn_prefix:
214+
return f"{self.fqdn_prefix}.apphub.backend.ai"
215+
return self.public_facing_address
216+
217+
@property
218+
def app_address(self) -> str:
219+
if self.fqdn_prefix:
220+
return f"{self.fqdn_prefix}.app.backend.ai"
221+
return self.public_facing_address
222+
223+
@property
224+
def wildcard_domain(self) -> Optional[str]:
225+
if self.fqdn_prefix:
226+
return f".{self.fqdn_prefix}.app.backend.ai"
227+
return None
228+
229+
@property
230+
def storage_public_address(self) -> str:
231+
if self.fqdn_prefix:
232+
return f"{self.fqdn_prefix}.public.isla-sorna.backend.ai"
233+
return self.public_facing_address

0 commit comments

Comments
 (0)