Skip to content

Commit 08183c7

Browse files
aarthy-dkclaude
andcommitted
feat(installer): add SSL/HTTPS support to Observability install
Add --ssl-cert-file / --ssl-key-file to `obs install`, mirroring the existing TestGen SSL flow. When both are provided, the cert+key are bind-mounted into the UI container and SSL_CERT_FILE/SSL_KEY_FILE are set so its nginx serves HTTPS; the printed service URLs switch to https. All Observability service URLs are proxied through the UI nginx, so applying TLS there covers every displayed endpoint. Align the two products' SSL codepaths: - Validate the cert/key arg pair in the compose step's pre_execute so a mismatch fails fast in the validation phase (parity with TestGen), rather than mid-install. - Record the `used_custom_cert` analytics property as a bool on both products; TestGen previously stored the key-file path string. Add Observability SSL test coverage matching TestGen: arg-pair mismatch aborts, compose contains the SSL env + bind mounts when enabled, and the placeholders are stripped when disabled. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6be5e37 commit 08183c7

2 files changed

Lines changed: 91 additions & 3 deletions

File tree

dk-installer.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1737,9 +1737,10 @@ def execute(self, action, args):
17371737

17381738
def on_action_success(self, action, args):
17391739
cred_file_path = action.data_folder.joinpath(CREDENTIALS_FILE.format(args.prod))
1740+
protocol = "https" if args.ssl_cert_file and args.ssl_key_file else "http"
17401741
with CONSOLE.tee(cred_file_path) as console_tee:
17411742
for service, url_tpl in OBS_SERVICES_URLS:
1742-
console_tee(f"{service:>20}: {url_tpl.format('http://localhost', args.port)}")
1743+
console_tee(f"{service:>20}: {url_tpl.format(f'{protocol}://localhost', args.port)}")
17431744
console_tee("")
17441745
console_tee(f"Username: {self._user_data['username']}")
17451746
console_tee(f"Password: {self._user_data['password']}", skip_logging=True)
@@ -1769,13 +1770,20 @@ def execute(self, action, args):
17691770

17701771

17711772
class ObsCreateComposeFileStep(CreateComposeFileStepBase):
1773+
def pre_execute(self, action, args):
1774+
super().pre_execute(action, args)
1775+
if bool(args.ssl_cert_file) != bool(args.ssl_key_file):
1776+
CONSOLE.msg("Both --ssl-cert-file and --ssl-key-file must be provided to use SSL certificates.")
1777+
raise AbortAction
1778+
17721779
def get_compose_file_contents(self, action, args):
17731780
action.analytics.additional_properties["used_custom_image"] = any(
17741781
(
17751782
args.ui_image != OBS_DEF_UI_IMAGE,
17761783
args.be_image != OBS_DEF_BE_IMAGE,
17771784
)
17781785
)
1786+
action.analytics.additional_properties["used_custom_cert"] = bool(args.ssl_cert_file and args.ssl_key_file)
17791787
compose_file_content = textwrap.dedent(
17801788
"""
17811789
name: ${DK_OBSERVABILITY_COMPOSE_NAME:-}
@@ -1880,12 +1888,14 @@ def get_compose_file_contents(self, action, args):
18801888
condition: service_healthy
18811889
environment:
18821890
OBSERVABILITY_AUTH_METHOD: ${DK_OBSERVABILITY_AUTH_METHOD:-basic}
1891+
__SSL_UI_ENVIRONMENT__
18831892
links:
18841893
- "observability_backend:observability-api"
18851894
- "observability_backend:event-api"
18861895
- "observability_backend:agent-api"
18871896
ports:
18881897
- "${DK_OBSERVABILITY_HTTP_PORT:-8082}:8082"
1898+
__SSL_UI_VOLUMES__
18891899
18901900
networks:
18911901
datakitchen:
@@ -1913,6 +1923,28 @@ def get_compose_file_contents(self, action, args):
19131923
compose_file_content,
19141924
)
19151925

1926+
# Fill (or strip) the UI TLS placeholders. When a cert+key are provided,
1927+
# they are bind-mounted into the UI container and SSL_CERT_FILE/SSL_KEY_FILE
1928+
# are set so its nginx serves HTTPS; otherwise the UI stays on plain HTTP.
1929+
if args.ssl_cert_file and args.ssl_key_file:
1930+
compose_file_content = compose_file_content.replace(
1931+
" __SSL_UI_ENVIRONMENT__",
1932+
" SSL_CERT_FILE: /dk/ssl/cert.crt\n SSL_KEY_FILE: /dk/ssl/cert.key",
1933+
)
1934+
compose_file_content = compose_file_content.replace(
1935+
" __SSL_UI_VOLUMES__",
1936+
" volumes:\n"
1937+
f" - type: bind\n"
1938+
f" source: {args.ssl_cert_file}\n"
1939+
f" target: /dk/ssl/cert.crt\n"
1940+
f" - type: bind\n"
1941+
f" source: {args.ssl_key_file}\n"
1942+
f" target: /dk/ssl/cert.key",
1943+
)
1944+
else:
1945+
compose_file_content = compose_file_content.replace(" __SSL_UI_ENVIRONMENT__\n", "")
1946+
compose_file_content = compose_file_content.replace(" __SSL_UI_VOLUMES__\n", "")
1947+
19161948
return compose_file_content
19171949

19181950

@@ -1967,6 +1999,20 @@ def get_parser(self, sub_parsers):
19671999
default=OBS_DEF_UI_IMAGE,
19682000
help="Observability UI image to use for the install. Defaults to %(default)s",
19692001
)
2002+
parser.add_argument(
2003+
"--ssl-cert-file",
2004+
dest="ssl_cert_file",
2005+
action="store",
2006+
default=None,
2007+
help="Path to SSL certificate file. When provided together with --ssl-key-file, the UI serves HTTPS.",
2008+
)
2009+
parser.add_argument(
2010+
"--ssl-key-file",
2011+
dest="ssl_key_file",
2012+
action="store",
2013+
default=None,
2014+
help="Path to SSL key file. When provided together with --ssl-cert-file, the UI serves HTTPS.",
2015+
)
19702016

19712017

19722018
class DemoContainerAction(Action):
@@ -2219,7 +2265,7 @@ def get_credentials_from_compose_file(self, file_contents):
22192265
return username, password
22202266

22212267
def get_compose_file_contents(self, action, args):
2222-
action.analytics.additional_properties["used_custom_cert"] = args.ssl_cert_file and args.ssl_key_file
2268+
action.analytics.additional_properties["used_custom_cert"] = bool(args.ssl_cert_file and args.ssl_key_file)
22232269
action.analytics.additional_properties["used_custom_image"] = args.image != TESTGEN_DEFAULT_IMAGE
22242270

22252271
ssl_variables = (

tests/test_obs_install.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import pytest
77

8-
from tests.installer import ObsInstallAction, AbortAction, ComposeVerifyExistingInstallStep
8+
from tests.installer import ObsInstallAction, AbortAction, ComposeVerifyExistingInstallStep, ObsCreateComposeFileStep
99

1010

1111
@pytest.fixture
@@ -72,3 +72,45 @@ def test_obs_existing_install_abort(obs_install_action, compose_path, stdout_moc
7272
with patch.object(obs_install_action, "steps", new=[ComposeVerifyExistingInstallStep]):
7373
with pytest.raises(AbortAction):
7474
obs_install_action.execute()
75+
76+
77+
@pytest.mark.integration
78+
@pytest.mark.parametrize("arg_to_set", ("ssl_cert_file", "ssl_key_file"))
79+
def test_obs_create_compose_file_abort_args(arg_to_set, obs_install_action, args_mock, console_msg_mock):
80+
setattr(args_mock, arg_to_set, "/some/file/path")
81+
82+
with patch.object(obs_install_action, "steps", new=[ObsCreateComposeFileStep]):
83+
with pytest.raises(AbortAction):
84+
obs_install_action.execute()
85+
86+
console_msg_mock.assert_any_msg_contains(
87+
"Both --ssl-cert-file and --ssl-key-file must be provided to use SSL certificates.",
88+
)
89+
90+
91+
@pytest.mark.integration
92+
def test_obs_compose_contains_ssl(obs_install_action, args_mock, compose_path):
93+
args_mock.ssl_cert_file = "/path/to/cert.crt"
94+
args_mock.ssl_key_file = "/path/to/cert.key"
95+
96+
with patch.object(obs_install_action, "steps", new=[ObsCreateComposeFileStep]):
97+
obs_install_action.execute()
98+
99+
contents = compose_path.read_text()
100+
assert "SSL_CERT_FILE: /dk/ssl/cert.crt" in contents
101+
assert "SSL_KEY_FILE: /dk/ssl/cert.key" in contents
102+
assert "source: /path/to/cert.crt" in contents
103+
assert "source: /path/to/cert.key" in contents
104+
assert "__SSL_UI_ENVIRONMENT__" not in contents
105+
assert "__SSL_UI_VOLUMES__" not in contents
106+
107+
108+
@pytest.mark.integration
109+
def test_obs_compose_without_ssl(obs_install_action, args_mock, compose_path):
110+
with patch.object(obs_install_action, "steps", new=[ObsCreateComposeFileStep]):
111+
obs_install_action.execute()
112+
113+
contents = compose_path.read_text()
114+
assert "SSL_CERT_FILE" not in contents
115+
assert "__SSL_UI_ENVIRONMENT__" not in contents
116+
assert "__SSL_UI_VOLUMES__" not in contents

0 commit comments

Comments
 (0)