diff --git a/dk-installer.py b/dk-installer.py index 08f5084..02f2ba5 100755 --- a/dk-installer.py +++ b/dk-installer.py @@ -1737,9 +1737,10 @@ def execute(self, action, args): def on_action_success(self, action, args): cred_file_path = action.data_folder.joinpath(CREDENTIALS_FILE.format(args.prod)) + protocol = "https" if args.ssl_cert_file and args.ssl_key_file else "http" with CONSOLE.tee(cred_file_path) as console_tee: for service, url_tpl in OBS_SERVICES_URLS: - console_tee(f"{service:>20}: {url_tpl.format('http://localhost', args.port)}") + console_tee(f"{service:>20}: {url_tpl.format(f'{protocol}://localhost', args.port)}") console_tee("") console_tee(f"Username: {self._user_data['username']}") console_tee(f"Password: {self._user_data['password']}", skip_logging=True) @@ -1769,6 +1770,12 @@ def execute(self, action, args): class ObsCreateComposeFileStep(CreateComposeFileStepBase): + def pre_execute(self, action, args): + super().pre_execute(action, args) + if bool(args.ssl_cert_file) != bool(args.ssl_key_file): + CONSOLE.msg("Both --ssl-cert-file and --ssl-key-file must be provided to use SSL certificates.") + raise AbortAction + def get_compose_file_contents(self, action, args): action.analytics.additional_properties["used_custom_image"] = any( ( @@ -1776,6 +1783,7 @@ def get_compose_file_contents(self, action, args): args.be_image != OBS_DEF_BE_IMAGE, ) ) + action.analytics.additional_properties["used_custom_cert"] = bool(args.ssl_cert_file and args.ssl_key_file) compose_file_content = textwrap.dedent( """ name: ${DK_OBSERVABILITY_COMPOSE_NAME:-} @@ -1880,12 +1888,14 @@ def get_compose_file_contents(self, action, args): condition: service_healthy environment: OBSERVABILITY_AUTH_METHOD: ${DK_OBSERVABILITY_AUTH_METHOD:-basic} + __SSL_UI_ENVIRONMENT__ links: - "observability_backend:observability-api" - "observability_backend:event-api" - "observability_backend:agent-api" ports: - "${DK_OBSERVABILITY_HTTP_PORT:-8082}:8082" + __SSL_UI_VOLUMES__ networks: datakitchen: @@ -1913,6 +1923,28 @@ def get_compose_file_contents(self, action, args): compose_file_content, ) + # Fill (or strip) the UI TLS placeholders. When a cert+key are provided, + # they are bind-mounted into the UI container and SSL_CERT_FILE/SSL_KEY_FILE + # are set so its nginx serves HTTPS; otherwise the UI stays on plain HTTP. + if args.ssl_cert_file and args.ssl_key_file: + compose_file_content = compose_file_content.replace( + " __SSL_UI_ENVIRONMENT__", + " SSL_CERT_FILE: /dk/ssl/cert.crt\n SSL_KEY_FILE: /dk/ssl/cert.key", + ) + compose_file_content = compose_file_content.replace( + " __SSL_UI_VOLUMES__", + " volumes:\n" + f" - type: bind\n" + f" source: {args.ssl_cert_file}\n" + f" target: /dk/ssl/cert.crt\n" + f" - type: bind\n" + f" source: {args.ssl_key_file}\n" + f" target: /dk/ssl/cert.key", + ) + else: + compose_file_content = compose_file_content.replace(" __SSL_UI_ENVIRONMENT__\n", "") + compose_file_content = compose_file_content.replace(" __SSL_UI_VOLUMES__\n", "") + return compose_file_content @@ -1967,6 +1999,20 @@ def get_parser(self, sub_parsers): default=OBS_DEF_UI_IMAGE, help="Observability UI image to use for the install. Defaults to %(default)s", ) + parser.add_argument( + "--ssl-cert-file", + dest="ssl_cert_file", + action="store", + default=None, + help="Path to SSL certificate file. When provided together with --ssl-key-file, the UI serves HTTPS.", + ) + parser.add_argument( + "--ssl-key-file", + dest="ssl_key_file", + action="store", + default=None, + help="Path to SSL key file. When provided together with --ssl-cert-file, the UI serves HTTPS.", + ) class DemoContainerAction(Action): @@ -2219,7 +2265,7 @@ def get_credentials_from_compose_file(self, file_contents): return username, password def get_compose_file_contents(self, action, args): - action.analytics.additional_properties["used_custom_cert"] = args.ssl_cert_file and args.ssl_key_file + action.analytics.additional_properties["used_custom_cert"] = bool(args.ssl_cert_file and args.ssl_key_file) action.analytics.additional_properties["used_custom_image"] = args.image != TESTGEN_DEFAULT_IMAGE ssl_variables = ( diff --git a/tests/test_obs_install.py b/tests/test_obs_install.py index 188771d..604e7ea 100644 --- a/tests/test_obs_install.py +++ b/tests/test_obs_install.py @@ -5,7 +5,7 @@ import pytest -from tests.installer import ObsInstallAction, AbortAction, ComposeVerifyExistingInstallStep +from tests.installer import ObsInstallAction, AbortAction, ComposeVerifyExistingInstallStep, ObsCreateComposeFileStep @pytest.fixture @@ -72,3 +72,45 @@ def test_obs_existing_install_abort(obs_install_action, compose_path, stdout_moc with patch.object(obs_install_action, "steps", new=[ComposeVerifyExistingInstallStep]): with pytest.raises(AbortAction): obs_install_action.execute() + + +@pytest.mark.integration +@pytest.mark.parametrize("arg_to_set", ("ssl_cert_file", "ssl_key_file")) +def test_obs_create_compose_file_abort_args(arg_to_set, obs_install_action, args_mock, console_msg_mock): + setattr(args_mock, arg_to_set, "/some/file/path") + + with patch.object(obs_install_action, "steps", new=[ObsCreateComposeFileStep]): + with pytest.raises(AbortAction): + obs_install_action.execute() + + console_msg_mock.assert_any_msg_contains( + "Both --ssl-cert-file and --ssl-key-file must be provided to use SSL certificates.", + ) + + +@pytest.mark.integration +def test_obs_compose_contains_ssl(obs_install_action, args_mock, compose_path): + args_mock.ssl_cert_file = "/path/to/cert.crt" + args_mock.ssl_key_file = "/path/to/cert.key" + + with patch.object(obs_install_action, "steps", new=[ObsCreateComposeFileStep]): + obs_install_action.execute() + + contents = compose_path.read_text() + assert "SSL_CERT_FILE: /dk/ssl/cert.crt" in contents + assert "SSL_KEY_FILE: /dk/ssl/cert.key" in contents + assert "source: /path/to/cert.crt" in contents + assert "source: /path/to/cert.key" in contents + assert "__SSL_UI_ENVIRONMENT__" not in contents + assert "__SSL_UI_VOLUMES__" not in contents + + +@pytest.mark.integration +def test_obs_compose_without_ssl(obs_install_action, args_mock, compose_path): + with patch.object(obs_install_action, "steps", new=[ObsCreateComposeFileStep]): + obs_install_action.execute() + + contents = compose_path.read_text() + assert "SSL_CERT_FILE" not in contents + assert "__SSL_UI_ENVIRONMENT__" not in contents + assert "__SSL_UI_VOLUMES__" not in contents