Skip to content

Commit 972c7ac

Browse files
authored
fix: allow custom ports on install setup optionally (#580)
1 parent 9f34c28 commit 972c7ac

File tree

10 files changed

+169
-17
lines changed

10 files changed

+169
-17
lines changed

cli/app/commands/install/base.py

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
import os
22
import re
33

4-
from app.utils.config import Config
4+
from app.utils.config import (
5+
API_PORT,
6+
CADDY_ADMIN_PORT,
7+
CADDY_HTTP_PORT,
8+
CADDY_HTTPS_PORT,
9+
Config,
10+
PROXY_PORT,
11+
SUPERTOKENS_API_PORT,
12+
VIEW_PORT,
13+
)
514
from app.utils.protocols import LoggerProtocol
615

716
from .messages import configuration_key_has_no_default_value
@@ -20,6 +29,14 @@ def __init__(
2029
config_file: str = None,
2130
repo: str = None,
2231
branch: str = None,
32+
api_port: int = None,
33+
view_port: int = None,
34+
db_port: int = None,
35+
redis_port: int = None,
36+
caddy_admin_port: int = None,
37+
caddy_http_port: int = None,
38+
caddy_https_port: int = None,
39+
supertokens_port: int = None,
2340
):
2441
self.logger = logger
2542
self.verbose = verbose
@@ -29,13 +46,71 @@ def __init__(
2946
self.config_file = config_file
3047
self.repo = repo
3148
self.branch = branch
49+
self.api_port = api_port
50+
self.view_port = view_port
51+
self.db_port = db_port
52+
self.redis_port = redis_port
53+
self.caddy_admin_port = caddy_admin_port
54+
self.caddy_http_port = caddy_http_port
55+
self.caddy_https_port = caddy_https_port
56+
self.supertokens_port = supertokens_port
3257
self._config = Config()
3358
self._config.load_user_config(self.config_file)
59+
self._user_config = None
3460
self.progress = None
3561
self.main_task = None
3662

3763
def _get_config(self, path: str):
3864
"""Base config getter - override in subclasses for specific behavior"""
65+
# Override port values if provided via command line
66+
if path == API_PORT and self.api_port is not None:
67+
return str(self.api_port)
68+
if path == VIEW_PORT and self.view_port is not None:
69+
return str(self.view_port)
70+
if path == "services.db.env.DB_PORT" and self.db_port is not None:
71+
return str(self.db_port)
72+
if path == "services.redis.env.REDIS_PORT" and self.redis_port is not None:
73+
return str(self.redis_port)
74+
75+
# Handle PROXY_PORT (which is an alias for CADDY_ADMIN_PORT)
76+
if path == PROXY_PORT:
77+
if self.caddy_admin_port is not None:
78+
return str(self.caddy_admin_port)
79+
# Fall back to CADDY_ADMIN_PORT from config, default to 2019
80+
try:
81+
return str(self._config.get(CADDY_ADMIN_PORT))
82+
except (KeyError, ValueError):
83+
return "2019"
84+
85+
if path == CADDY_ADMIN_PORT and self.caddy_admin_port is not None:
86+
return str(self.caddy_admin_port)
87+
if path == CADDY_HTTP_PORT and self.caddy_http_port is not None:
88+
return str(self.caddy_http_port)
89+
if path == CADDY_HTTPS_PORT and self.caddy_https_port is not None:
90+
return str(self.caddy_https_port)
91+
if path == SUPERTOKENS_API_PORT and self.supertokens_port is not None:
92+
return str(self.supertokens_port)
93+
94+
# Handle simple key lookups for port overrides
95+
if path == "db_port" and self.db_port is not None:
96+
return str(self.db_port)
97+
if path == "redis_port" and self.redis_port is not None:
98+
return str(self.redis_port)
99+
if path == "proxy_port" and self.caddy_admin_port is not None:
100+
return str(self.caddy_admin_port)
101+
if path == "api_port" and self.api_port is not None:
102+
return str(self.api_port)
103+
if path == "view_port" and self.view_port is not None:
104+
return str(self.view_port)
105+
if path == "caddy_admin_port" and self.caddy_admin_port is not None:
106+
return str(self.caddy_admin_port)
107+
if path == "caddy_http_port" and self.caddy_http_port is not None:
108+
return str(self.caddy_http_port)
109+
if path == "caddy_https_port" and self.caddy_https_port is not None:
110+
return str(self.caddy_https_port)
111+
if path == "supertokens_api_port" and self.supertokens_port is not None:
112+
return str(self.supertokens_port)
113+
39114
return self._config.get(path)
40115

41116
def _validate_domains(self, api_domain: str = None, view_domain: str = None):

cli/app/commands/install/development.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -82,15 +82,15 @@ def __init__(
8282
config_file=config_file,
8383
repo=repo,
8484
branch=branch,
85+
api_port=api_port,
86+
view_port=view_port,
87+
db_port=db_port,
88+
redis_port=redis_port,
89+
caddy_admin_port=caddy_admin_port,
90+
caddy_http_port=caddy_http_port,
91+
caddy_https_port=caddy_https_port,
92+
supertokens_port=supertokens_port,
8593
)
86-
self.api_port = api_port
87-
self.view_port = view_port
88-
self.db_port = db_port
89-
self.redis_port = redis_port
90-
self.caddy_admin_port = caddy_admin_port
91-
self.caddy_http_port = caddy_http_port
92-
self.caddy_https_port = caddy_https_port
93-
self.supertokens_port = supertokens_port
9494

9595
# safe fallback incase cwd is not accessible
9696
if install_path:

cli/app/commands/install/run.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@
1919
from app.utils.config import (
2020
API_ENV_FILE,
2121
API_PORT,
22+
CADDY_ADMIN_PORT,
2223
CADDY_CONFIG_VOLUME,
24+
CADDY_HTTP_PORT,
25+
CADDY_HTTPS_PORT,
2326
DEFAULT_BRANCH,
2427
DEFAULT_COMPOSE_FILE,
2528
DEFAULT_PATH,
@@ -140,13 +143,22 @@ def _get_config(self, path: str):
140143
return str(self.db_port)
141144
if path == "services.redis.env.REDIS_PORT" and self.redis_port is not None:
142145
return str(self.redis_port)
143-
if path == PROXY_PORT and self.caddy_admin_port is not None:
144-
return str(self.caddy_admin_port)
145-
if path == "services.caddy.env.CADDY_ADMIN_PORT" and self.caddy_admin_port is not None:
146+
147+
# Handle PROXY_PORT (which is an alias for CADDY_ADMIN_PORT)
148+
if path == PROXY_PORT:
149+
if self.caddy_admin_port is not None:
150+
return str(self.caddy_admin_port)
151+
# Fall back to CADDY_ADMIN_PORT from config, default to 2019
152+
try:
153+
return str(_config.get(CADDY_ADMIN_PORT))
154+
except (KeyError, ValueError):
155+
return "2019"
156+
157+
if path == CADDY_ADMIN_PORT and self.caddy_admin_port is not None:
146158
return str(self.caddy_admin_port)
147-
if path == "services.caddy.env.CADDY_HTTP_PORT" and self.caddy_http_port is not None:
159+
if path == CADDY_HTTP_PORT and self.caddy_http_port is not None:
148160
return str(self.caddy_http_port)
149-
if path == "services.caddy.env.CADDY_HTTPS_PORT" and self.caddy_https_port is not None:
161+
if path == CADDY_HTTPS_PORT and self.caddy_https_port is not None:
150162
return str(self.caddy_https_port)
151163
if path == SUPERTOKENS_API_PORT and self.supertokens_port is not None:
152164
return str(self.supertokens_port)
@@ -465,6 +477,12 @@ def _start_services(self):
465477

466478
def _load_proxy(self):
467479
proxy_port = self._get_config(PROXY_PORT)
480+
# Ensure proxy_port is an integer
481+
try:
482+
proxy_port = int(proxy_port)
483+
except (ValueError, TypeError):
484+
proxy_port = 2019 # Default fallback
485+
468486
full_source_path = self._get_config("full_source_path")
469487
caddy_json_config = os.path.join(full_source_path, "helpers", "caddy.json")
470488
config = LoadConfig(

cli/app/commands/proxy/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ def load_config(self, config_file: str, port: int = proxy_port) -> tuple[bool, s
179179
error_msg = invalid_json_error.format(error=str(e))
180180
self.logger.debug(error_msg)
181181
return False, error_msg
182-
except requests.exceptions.ConnectionError:
182+
except requests.exceptions.ConnectionError as e:
183183
error_msg = caddy_connection_failed.format(error=str(e))
184184
self.logger.debug(error_msg)
185185
return False, error_msg

cli/app/commands/proxy/load.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ def execute(self) -> LoadResult:
104104
if not self.config.config_file:
105105
return self._create_result(False, "Configuration file is required")
106106

107+
# In dry-run mode, just return success without actually connecting to Caddy
108+
if self.config.dry_run:
109+
return self._create_result(True, None)
110+
107111
success, message = self.caddy_service.load_config_file(self.config.config_file, self.config.proxy_port)
108112
return self._create_result(success, None if success else message)
109113

cli/app/commands/proxy/status.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ def status(self) -> StatusResult:
8383
return self.execute()
8484

8585
def execute(self) -> StatusResult:
86+
# In dry-run mode, just return success without actually connecting to Caddy
87+
if self.config.dry_run:
88+
return self._create_result(True, None)
89+
8690
success, message = self.caddy_service.get_status(self.config.proxy_port)
8791
return self._create_result(success, None if success else message)
8892

cli/app/commands/proxy/stop.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ def stop(self) -> StopResult:
9090
return self.execute()
9191

9292
def execute(self) -> StopResult:
93+
# In dry-run mode, just return success without actually connecting to Caddy
94+
if self.config.dry_run:
95+
return self._create_result(True, None)
96+
9397
success, message = self.caddy_service.stop_caddy(self.config.proxy_port)
9498
return self._create_result(success, None if success else message)
9599

cli/app/utils/config.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,14 +98,22 @@ def unflatten_config(self, flattened_config: dict) -> dict:
9898

9999
def expand_env_placeholders(value: str) -> str:
100100
# Expand environment placeholders in the form ${ENV_VAR:-default}
101+
# Supports nested expansions like ${VAR1:-${VAR2:-default}}
101102
pattern = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)(:-([^}]*))?}")
103+
max_iterations = 10 # Prevent infinite loops
104+
iteration = 0
102105

103106
def replacer(match):
104107
var_name = match.group(1)
105108
default = match.group(3) if match.group(2) else ""
106109
return os.environ.get(var_name, default)
107110

108-
return pattern.sub(replacer, value)
111+
# Keep expanding until no more placeholders are found or max iterations reached
112+
while pattern.search(value) and iteration < max_iterations:
113+
value = pattern.sub(replacer, value)
114+
iteration += 1
115+
116+
return value
109117

110118

111119
VIEW_ENV_FILE = "services.view.env.VIEW_ENV_FILE"
@@ -130,5 +138,8 @@ def replacer(match):
130138
VIEW_PORT = "services.view.env.NEXT_PUBLIC_PORT"
131139
API_PORT = "services.api.env.PORT"
132140
CADDY_CONFIG_VOLUME = "services.caddy.env.CADDY_CONFIG_VOLUME"
141+
CADDY_ADMIN_PORT = "services.caddy.env.CADDY_ADMIN_PORT"
142+
CADDY_HTTP_PORT = "services.caddy.env.CADDY_HTTP_PORT"
143+
CADDY_HTTPS_PORT = "services.caddy.env.CADDY_HTTPS_PORT"
133144
DOCKER_PORT = "services.api.env.DOCKER_PORT"
134145
SUPERTOKENS_API_PORT = "services.api.env.SUPERTOKENS_API_PORT"

cli/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "nixopus"
3-
version = "0.1.25"
3+
version = "0.1.26"
44
description = "A CLI for Nixopus"
55
authors = ["Nixopus <[email protected]>"]
66
readme = "README.md"

cli/tests/utils/test_config.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,42 @@ def test_expand_env_placeholders_real_world_example(self):
360360
result = expand_env_placeholders("${API_PORT:-8443} and ${DB_NAME:-postgres}")
361361
self.assertEqual(result, "9000 and production_db")
362362

363+
def test_expand_env_placeholders_nested_single_level(self):
364+
# Test nested expansion: ${VAR1:-${VAR2:-default}}
365+
# Neither VAR1 nor VAR2 is set, should use final default
366+
result = expand_env_placeholders("${VAR1:-${VAR2:-2019}}")
367+
self.assertEqual(result, "2019")
368+
369+
def test_expand_env_placeholders_nested_first_var_set(self):
370+
# First variable is set, should use it
371+
os.environ["PROXY_PORT"] = "3000"
372+
result = expand_env_placeholders("${PROXY_PORT:-${CADDY_ADMIN_PORT:-2019}}")
373+
self.assertEqual(result, "3000")
374+
375+
def test_expand_env_placeholders_nested_second_var_set(self):
376+
# First variable not set, second variable is set
377+
os.environ["CADDY_ADMIN_PORT"] = "2020"
378+
result = expand_env_placeholders("${PROXY_PORT:-${CADDY_ADMIN_PORT:-2019}}")
379+
self.assertEqual(result, "2020")
380+
381+
def test_expand_env_placeholders_nested_both_vars_set(self):
382+
# Both variables set, should use first one (higher priority)
383+
os.environ["PROXY_PORT"] = "3000"
384+
os.environ["CADDY_ADMIN_PORT"] = "2020"
385+
result = expand_env_placeholders("${PROXY_PORT:-${CADDY_ADMIN_PORT:-2019}}")
386+
self.assertEqual(result, "3000")
387+
388+
def test_expand_env_placeholders_nested_multiple_levels(self):
389+
# Test triple nesting
390+
result = expand_env_placeholders("${VAR1:-${VAR2:-${VAR3:-final}}}")
391+
self.assertEqual(result, "final")
392+
393+
def test_expand_env_placeholders_nested_in_url(self):
394+
# Test nested expansion in a URL context (real-world scenario)
395+
os.environ["CADDY_ADMIN_PORT"] = "2020"
396+
result = expand_env_placeholders("http://localhost:${PROXY_PORT:-${CADDY_ADMIN_PORT:-2019}}")
397+
self.assertEqual(result, "http://localhost:2020")
398+
363399

364400
if __name__ == "__main__":
365401
unittest.main()

0 commit comments

Comments
 (0)