diff --git a/.circleci/config.yml b/.circleci/config.yml index 0ad5f33cb72..9b7ac380c00 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -736,6 +736,27 @@ jobs: - run: | echo build executed successfully + test-launcher: + executor: self-hosted-amd + steps: + - checkout + - attach_workspace: + at: /tmp/workspace/<< pipeline.id >> + - run: + name: Extract python for launcher tests + command: | + tar -C /tmp/workspace/$CIRCLE_PIPELINE_ID/python-artifacts -zxvf /tmp/workspace/$CIRCLE_PIPELINE_ID/python-artifacts/all.tar.gz ./x86_64-unknown-linux-gnu + - run: + name: Test influxdb3-launcher + command: | + /tmp/workspace/$CIRCLE_PIPELINE_ID/python-artifacts/x86_64-unknown-linux-gnu/python/bin/python3 \ + .circleci/packages/test_influxdb3-launcher.py \ + .circleci/packages/influxdb3/fs/usr/lib/influxdb3/influxdb3-launcher + - run: + name: Cleanup extracted python for launcher tests + command: rm -rf /tmp/workspace/$CIRCLE_PIPELINE_ID/python-artifacts/x86_64-unknown-linux-gnu + when: always + workflows: version: 2 snapshot: @@ -804,6 +825,10 @@ workflows: - x86_64-unknown-linux-gnu requires: - fetch-python + - test-launcher: + <<: *nofork_filter + requires: + - fetch-python - doc: <<: *any_filter - build-release: diff --git a/.circleci/packages/influxdb3/fs/usr/lib/influxdb3/influxdb3-launcher b/.circleci/packages/influxdb3/fs/usr/lib/influxdb3/influxdb3-launcher index e10f0c0ec11..b6e33f3a268 100644 --- a/.circleci/packages/influxdb3/fs/usr/lib/influxdb3/influxdb3-launcher +++ b/.circleci/packages/influxdb3/fs/usr/lib/influxdb3/influxdb3-launcher @@ -24,7 +24,6 @@ sys.dont_write_bytecode = True # don't create __pycache__ files import argparse import os -import re import signal import tempfile import tomllib # this is in cpython 3.11+ standard library @@ -125,14 +124,6 @@ REQUIRED_TOML_KEYS = { ], } -REQUIRED_ENV_KEYS = { - "core": ["INFLUXDB3_OBJECT_STORE"], - "enterprise": [ - "INFLUXDB3_OBJECT_STORE,INFLUXDB3_ENTERPRISE_LICENSE_FILE", - "INFLUXDB3_OBJECT_STORE,INFLUXDB3_ENTERPRISE_LICENSE_EMAIL,INFLUXDB3_ENTERPRISE_LICENSE_TYPE", - ], -} - def read_stamp(stamp_path: str) -> str | None: """Read flavor from stamp file. Returns None if doesn't exist or can't read.""" @@ -319,89 +310,13 @@ def check_executable(path: str) -> str: return abs_path -def read_config_env(path: str, required: List[str] = []) -> Dict[str, str]: - """ - Read and parse the environment variable configuration file. - - Args: - path: Path to the environment variable configuration file - required: List of required variable groups. Each element can be: - - A single variable name (e.g., 'LICENSE_FILE') - - Comma-separated variable names (e.g., 'LICENSE_EMAIL,LICENSE_TYPE') - At least one group must be satisfied. - - Returns: - Dict of environment variables to set - """ - abs_path: str = _validate_file_path(path, "config file") - - env_vars: Dict[str, str] = {} - - # Variable name must start with letter or underscore, contain only - # uppercase, digits, underscores - var_pattern = re.compile(r"^([A-Z_][A-Z0-9_]*)=(.*)$") - - try: - with open(abs_path, "r", encoding="utf-8") as f: - for line in f: - line = line.rstrip() # Strip trailing whitespace - - if not line or line.startswith("#"): - continue # Skip empty and comment lines - - # Try to match environment variable pattern - match = var_pattern.match(line) - if match: - var_name = match.group(1) - var_value = match.group(2) - - # Strip surrounding quotes if present (double or single) - # Handles: FOO="bar" or FOO='bar' -> bar - # Preserves: FOO=bar -> bar - # Error on mismatched quotes - if len(var_value) >= 2: - has_opening_quote = var_value[0] in ('"', "'") - has_closing_quote = var_value[-1] in ('"', "'") - - if has_opening_quote or has_closing_quote: - # Check for matching quotes - if var_value[0] == '"' and var_value[-1] == '"': - var_value = var_value[1:-1] - elif var_value[0] == "'" and var_value[-1] == "'": - var_value = var_value[1:-1] - else: - # Mismatched or incomplete quotes - print( - f"E: Mismatched quotes in config file {path} for variable {var_name}={var_value}", - file=sys.stderr, - ) - sys.exit(1) - - env_vars[var_name] = var_value - - # Validate required variables if specified - if required: - _validate_required_keys(env_vars, required, "environment variable") - - return env_vars - except Exception as e: # pragma: nocover - print(f"E: problem reading config file {path}: {e}", file=sys.stderr) - sys.exit(1) - - -def read_config_toml( - path: str, flavor: str, required: List[str] = [] -) -> Dict[str, str]: +def read_config_toml(path: str, flavor: str) -> Dict[str, str]: """ Read and parse the TOML configuration file for environment variables. Args: path: Path to the TOML configuration file flavor: The InfluxDB 3 flavor ('core' or 'enterprise') - required: List of required TOML key groups. Each element can be: - - A single TOML key (e.g., 'license-file') - - Comma-separated TOML keys (e.g., 'license-email,license-type') - At least one group must be satisfied. Returns: Dict of environment variables to set @@ -413,6 +328,7 @@ def read_config_toml( toml_data: Dict[str, Any] = tomllib.load(f) # Validate required TOML keys before conversion + required = REQUIRED_TOML_KEYS[flavor] if required: _validate_required_keys(toml_data, required, "TOML key") @@ -449,36 +365,6 @@ def read_config_toml( sys.exit(1) -def read_config( - config_toml: str | None, - config_env: str | None, - flavor: str, -) -> Dict[str, str]: - """ - Read configuration from TOML or environment variable files. - - Args: - config_toml: Path to TOML configuration file (optional) - config_env: Path to environment variable configuration file (optional) - flavor: The InfluxDB 3 flavor ('core' or 'enterprise') - - Returns: - Dict of environment variables to set - - Priority: If config_toml is provided, it is used. Otherwise, config_env is used. - """ - env_vars: Dict[str, str] = {} - - if config_toml: - env_vars.update( - read_config_toml(config_toml, flavor, REQUIRED_TOML_KEYS[flavor]) - ) - elif config_env: - env_vars.update(read_config_env(config_env, REQUIRED_ENV_KEYS[flavor])) - - return env_vars - - def write_pidfile(pidfile: str, pid: int | None = None) -> None: """ Write a PID to a file atomically. @@ -714,11 +600,6 @@ def main(argv: List[str] | None = None) -> None: parser.add_argument( "--exec", required=True, metavar="PATH", help="Path to the influxdb3 executable" ) - parser.add_argument( - "--config-env", - metavar="PATH", - help="Path to the environment variable configuration file", - ) parser.add_argument( "--config-toml", metavar="PATH", help="Path to the TOML configuration file" ) @@ -747,15 +628,15 @@ def main(argv: List[str] | None = None) -> None: args = parser.parse_args(launcher_args) # Validate that at least one config option is provided - if not args.config_env and not args.config_toml: - parser.error("at least one of --config-env or --config-toml is required") + if not args.config_toml: + parser.error("--config-toml is required") # Warn if --log-file is used without --daemonize if args.log_file and not args.daemonize: print("W: --log-file has no effect without --daemonize", file=sys.stderr) # Read configuration files and merge environment variables - env_vars = read_config(args.config_toml, args.config_env, args.flavor) + env_vars = read_config_toml(args.config_toml, args.flavor) # Check flavor migration BEFORE exec stamp_path = os.path.join(args.stamp_dir, STAMP_FILENAME) diff --git a/.circleci/packages/test_influxdb3-launcher.py b/.circleci/packages/test_influxdb3-launcher.py index 9c2a15d131e..1d0da9fb8be 100644 --- a/.circleci/packages/test_influxdb3-launcher.py +++ b/.circleci/packages/test_influxdb3-launcher.py @@ -147,150 +147,6 @@ def test_windows_branch_skips_exec_bit(self): self.launcher.os.name = original_os_name # type: ignore[attr-defined] -class TestReadConfigEnv(unittest.TestCase): - """Tests for read_config_env() function""" - - def setUp(self): - self.launcher = load_launcher_module() - self.temp_dir = tempfile.mkdtemp() - - def tearDown(self): - shutil.rmtree(self.temp_dir, ignore_errors=True) - - def _write_config(self, content): - """Helper to write a config file and return its path""" - config_path = os.path.join(self.temp_dir, "test.conf") - with open(config_path, "w", encoding="utf-8") as f: - f.write(content) - return config_path - - def test_simple_variables(self): - """Test simple variable parsing""" - config_path = self._write_config("FOO=bar\nBAZ=qux\n") - env_vars = self.launcher.read_config_env(config_path) - self.assertEqual(env_vars, {"FOO": "bar", "BAZ": "qux"}) - - def test_double_quoted_values(self): - """Test double-quoted values""" - config_path = self._write_config('FOO="bar"\nBAZ="qux with spaces"\n') - env_vars = self.launcher.read_config_env(config_path) - self.assertEqual(env_vars, {"FOO": "bar", "BAZ": "qux with spaces"}) - - def test_single_quoted_values(self): - """Test single-quoted values""" - config_path = self._write_config("FOO='bar'\nBAZ='qux with spaces'\n") - env_vars = self.launcher.read_config_env(config_path) - self.assertEqual(env_vars, {"FOO": "bar", "BAZ": "qux with spaces"}) - - def test_mixed_quoting(self): - """Test mixed quoted and unquoted values""" - config_path = self._write_config('FOO=abc\nBAR="def"\nBAZ="efg \'hij"\n') - env_vars = self.launcher.read_config_env(config_path) - self.assertEqual(env_vars, {"FOO": "abc", "BAR": "def", "BAZ": "efg 'hij"}) - - def test_empty_value(self): - """Test empty values""" - config_path = self._write_config("FOO=\nBAR=value\n") - env_vars = self.launcher.read_config_env(config_path) - self.assertEqual(env_vars, {"FOO": "", "BAR": "value"}) - - def test_comments_ignored(self): - """Test that comments are ignored""" - config_path = self._write_config( - "# This is a comment\nFOO=bar\n# Another comment\nBAZ=qux\n" - ) - env_vars = self.launcher.read_config_env(config_path) - self.assertEqual(env_vars, {"FOO": "bar", "BAZ": "qux"}) - - def test_blank_lines_ignored(self): - """Test that blank lines are ignored""" - config_path = self._write_config("FOO=bar\n\n\nBAZ=qux\n") - env_vars = self.launcher.read_config_env(config_path) - self.assertEqual(env_vars, {"FOO": "bar", "BAZ": "qux"}) - - def test_invalid_lines_skipped(self): - """Test that invalid lines are skipped""" - config_path = self._write_config( - "FOO=bar\n" - "this is not valid\n" - "BAZ=qux\n" - "lowercase=ignored\n" - "MIX3d_Case=ignored\n" - "VALID=accepted\n" - ) - env_vars = self.launcher.read_config_env(config_path) - self.assertEqual(env_vars, {"FOO": "bar", "BAZ": "qux", "VALID": "accepted"}) - - def test_underscore_in_name(self): - """Test variable names with underscores""" - config_path = self._write_config( - "FOO_BAR=value1\n_LEADING=value2\nTRAILING_=value3\n" - ) - env_vars = self.launcher.read_config_env(config_path) - self.assertEqual( - env_vars, {"FOO_BAR": "value1", "_LEADING": "value2", "TRAILING_": "value3"} - ) - - def test_digits_in_name(self): - """Test variable names with digits""" - config_path = self._write_config("FOO123=value1\nBAR_456_BAZ=value2\n") - env_vars = self.launcher.read_config_env(config_path) - self.assertEqual(env_vars, {"FOO123": "value1", "BAR_456_BAZ": "value2"}) - - def test_nonexistent_config(self): - """Test with non-existent config file""" - with self.assertRaises(SystemExit) as cm: - self.launcher.read_config_env("/nonexistent/config.conf") - self.assertEqual(cm.exception.code, 1) - - def test_config_is_directory(self): - """Test with directory instead of file""" - with self.assertRaises(SystemExit) as cm: - self.launcher.read_config_env(self.temp_dir) - self.assertEqual(cm.exception.code, 1) - - def test_empty_config_file(self): - """Test with empty config file""" - config_path = self._write_config("") - env_vars = self.launcher.read_config_env(config_path) - self.assertEqual(env_vars, {}) - - def test_mismatched_quotes_opening(self): - """Test that opening quote without closing quote causes an error""" - config_path = self._write_config('FOO="bar\n') - with self.assertRaises(SystemExit) as cm: - self.launcher.read_config_env(config_path) - self.assertEqual(cm.exception.code, 1) - - def test_mismatched_quotes_closing(self): - """Test that closing quote without opening quote causes an error""" - config_path = self._write_config('FOO=bar"\n') - with self.assertRaises(SystemExit) as cm: - self.launcher.read_config_env(config_path) - self.assertEqual(cm.exception.code, 1) - - def test_mismatched_quote_types(self): - """Test that mixing single and double quotes causes an error""" - config_path = self._write_config("FOO=\"bar'\n") - with self.assertRaises(SystemExit) as cm: - self.launcher.read_config_env(config_path) - self.assertEqual(cm.exception.code, 1) - - def test_special_characters_in_value(self): - """Test special characters in values""" - config_path = self._write_config( - 'PATH="/usr/bin:/usr/local/bin"\nURL="https://example.com?foo=bar&baz=qux"\n' - ) - env_vars = self.launcher.read_config_env(config_path) - self.assertEqual( - env_vars, - { - "PATH": "/usr/bin:/usr/local/bin", - "URL": "https://example.com?foo=bar&baz=qux", - }, - ) - - class TestReadConfigTOML(unittest.TestCase): """Tests for read_config_toml() function""" @@ -423,10 +279,11 @@ def test_config_is_directory(self): self.assertEqual(cm.exception.code, 1) def test_empty_config_file(self): - """Test with empty TOML config file""" + """Test with empty TOML config file fails validation""" config_path = self._write_toml_config("") - env_vars = self.launcher.read_config_toml(config_path, "core") - self.assertEqual(env_vars, {}) + with self.assertRaises(SystemExit) as cm: + self.launcher.read_config_toml(config_path, "core") + self.assertEqual(cm.exception.code, 1) def test_invalid_toml(self): """Test with invalid TOML syntax""" @@ -600,7 +457,7 @@ def test_toml_nested_value_skipped(self): with open(toml_path, "w") as f: f.write('object-store = "file"\n[extra]\nfoo = "bar"\n') try: - result = self.launcher.read_config(toml_path, None, "core") + result = self.launcher.read_config_toml(toml_path, "core") self.assertEqual(result, {"INFLUXDB3_OBJECT_STORE": "file"}) finally: os.remove(toml_path) @@ -634,8 +491,8 @@ def test_enterprise_to_core_downgrade_fails(self): shutil.rmtree(temp_dir, ignore_errors=True) -class TestReadConfig(unittest.TestCase): - """Tests for read_config() function""" +class TestReadConfigTOMLValidation(unittest.TestCase): + """Tests for read_config_toml() with flavor-based validation""" def setUp(self): self.launcher = load_launcher_module() @@ -651,47 +508,18 @@ def _write_toml(self, content): f.write(content) return path - def _write_env(self, content): - """Helper to write ENV config""" - path = os.path.join(self.temp_dir, "test.env") - with open(path, "w") as f: - f.write(content) - return path - - def test_only_toml_core(self): - """Test with only TOML config for core flavor""" + def test_toml_core(self): + """Test with TOML config for core flavor""" toml_path = self._write_toml('object-store = "file"\n') - result = self.launcher.read_config(toml_path, None, "core") - self.assertEqual(result, {"INFLUXDB3_OBJECT_STORE": "file"}) - - def test_only_env_core(self): - """Test with only ENV config for core flavor""" - env_path = self._write_env("INFLUXDB3_OBJECT_STORE=file\n") - result = self.launcher.read_config(None, env_path, "core") + result = self.launcher.read_config_toml(toml_path, "core") self.assertEqual(result, {"INFLUXDB3_OBJECT_STORE": "file"}) - def test_both_toml_and_env_toml_takes_precedence(self): - """Test with both TOML and ENV, only TOML is read""" - toml_path = self._write_toml('object-store = "file"\nnode-id = "from-toml"\n') - env_path = self._write_env( - "INFLUXDB3_OBJECT_STORE=file\nINFLUXDB3_NODE_IDENTIFIER_PREFIX=from-env\n" - ) - result = self.launcher.read_config(toml_path, env_path, "core") - # TOML takes precedence, ENV is ignored - self.assertEqual( - result, - { - "INFLUXDB3_OBJECT_STORE": "file", - "INFLUXDB3_NODE_IDENTIFIER_PREFIX": "from-toml", - }, - ) - def test_enterprise_with_license_file(self): """Test enterprise flavor with license file""" toml_path = self._write_toml( 'object-store = "file"\nlicense-file = "/path/to/license.jwt"\n' ) - result = self.launcher.read_config(toml_path, None, "enterprise") + result = self.launcher.read_config_toml(toml_path, "enterprise") self.assertEqual( result, { @@ -702,12 +530,12 @@ def test_enterprise_with_license_file(self): def test_enterprise_with_license_email_and_type(self): """Test enterprise flavor with license email and type""" - env_path = self._write_env( - "INFLUXDB3_OBJECT_STORE=file\n" - "INFLUXDB3_ENTERPRISE_LICENSE_EMAIL=test@example.com\n" - "INFLUXDB3_ENTERPRISE_LICENSE_TYPE=trial\n" + toml_path = self._write_toml( + 'object-store = "file"\n' + 'license-email = "test@example.com"\n' + 'license-type = "trial"\n' ) - result = self.launcher.read_config(None, env_path, "enterprise") + result = self.launcher.read_config_toml(toml_path, "enterprise") self.assertEqual( result, { @@ -717,11 +545,6 @@ def test_enterprise_with_license_email_and_type(self): }, ) - def test_read_config_no_files_returns_empty(self): - """No config files should return empty env dict""" - result = self.launcher.read_config(None, None, "core") - self.assertEqual(result, {}) - class TestWritePidfile(unittest.TestCase): """Tests for write_pidfile() function""" @@ -1004,9 +827,9 @@ def fake_close(fd): original_dup2 = os.dup2 original_close = os.close original_write_pidfile = self.launcher.write_pidfile - original_platform = self.launcher.PLATFORM_SUPPORTS_FORK + original_platform = getattr(self.launcher, "PLATFORM_SUPPORTS_FORK") try: - self.launcher.PLATFORM_SUPPORTS_FORK = True + setattr(self.launcher, "PLATFORM_SUPPORTS_FORK", True) os.fork = fake_fork # type: ignore[assignment] os.setsid = fake_setsid # type: ignore[assignment] signal.signal = fake_signal # type: ignore[assignment] @@ -1033,7 +856,7 @@ def fake_close(fd): os.dup2 = original_dup2 # type: ignore[assignment] os.close = original_close # type: ignore[assignment] self.launcher.write_pidfile = original_write_pidfile # type: ignore[assignment] - self.launcher.PLATFORM_SUPPORTS_FORK = original_platform + setattr(self.launcher, "PLATFORM_SUPPORTS_FORK", original_platform) def test_daemonize_unsupported_platform(self): """Test that daemonize fails gracefully on unsupported platforms""" @@ -1155,11 +978,11 @@ def mock_execve(path, args, env): ) def test_run_restores_sighup_before_exec(self): """run() should reset SIGHUP to default if it was ignored""" - called_handler = {"handler": None} + called_handler: list = [None] def mock_execve(path, args, env): # Capture handler at exec time - called_handler["handler"] = signal.getsignal(signal.SIGHUP) + called_handler[0] = signal.getsignal(signal.SIGHUP) raise SystemExit(0) original_handler = signal.getsignal(signal.SIGHUP) @@ -1171,7 +994,7 @@ def mock_execve(path, args, env): self.launcher.run("/usr/bin/influxdb3", [], {}) self.assertEqual(cm.exception.code, 0) - self.assertEqual(called_handler["handler"], signal.SIG_DFL) + self.assertEqual(called_handler[0], signal.SIG_DFL) finally: signal.signal(signal.SIGHUP, original_handler) @@ -1180,10 +1003,10 @@ def mock_execve(path, args, env): ) def test_run_keeps_sighup_default(self): """If SIGHUP already default, run() should leave it unchanged""" - called_handler = {"handler": None} + called_handler: list = [None] def mock_execve(path, args, env): - called_handler["handler"] = signal.getsignal(signal.SIGHUP) + called_handler[0] = signal.getsignal(signal.SIGHUP) raise SystemExit(0) original_handler = signal.getsignal(signal.SIGHUP) @@ -1195,7 +1018,7 @@ def mock_execve(path, args, env): self.launcher.run("/usr/bin/influxdb3", [], {}) self.assertEqual(cm.exception.code, 0) - self.assertEqual(called_handler["handler"], signal.SIG_DFL) + self.assertEqual(called_handler[0], signal.SIG_DFL) finally: signal.signal(signal.SIGHUP, original_handler) @@ -1227,9 +1050,9 @@ def _create_executable(self): os.chmod(exec_path, 0o755) return exec_path - def _create_config(self, content): - """Create a config file and return its path""" - config_path = os.path.join(self.temp_dir, "test.conf") + def _create_toml_config(self, content): + """Create a TOML config file and return its path""" + config_path = os.path.join(self.temp_dir, "test.toml") with open(config_path, "w") as f: f.write(content) return config_path @@ -1237,13 +1060,13 @@ def _create_config(self, content): def test_missing_required_flavor(self): """Test that missing --flavor causes error""" exec_path = self._create_executable() - config_path = self._create_config("INFLUXDB3_OBJECT_STORE=file\n") + config_path = self._create_toml_config('object-store = "file"\n') argv = [ "launcher", "--exec", exec_path, - "--config-env", + "--config-toml", config_path, "--stamp-dir", self.temp_dir, @@ -1266,13 +1089,13 @@ def test_missing_required_flavor(self): def test_missing_required_exec(self): """Test that missing --exec causes error""" - config_path = self._create_config("INFLUXDB3_OBJECT_STORE=file\n") + config_path = self._create_toml_config('object-store = "file"\n') argv = [ "launcher", "--flavor", "core", - "--config-env", + "--config-toml", config_path, "--stamp-dir", self.temp_dir, @@ -1286,7 +1109,7 @@ def test_missing_required_exec(self): def test_missing_required_stamp_dir(self): """Test that missing --stamp-dir causes error""" exec_path = self._create_executable() - config_path = self._create_config("INFLUXDB3_OBJECT_STORE=file\n") + config_path = self._create_toml_config('object-store = "file"\n') argv = [ "launcher", @@ -1294,7 +1117,7 @@ def test_missing_required_stamp_dir(self): "core", "--exec", exec_path, - "--config-env", + "--config-toml", config_path, "--", ] @@ -1306,7 +1129,7 @@ def test_missing_required_stamp_dir(self): def test_invalid_flavor(self): """Test that invalid flavor value causes error""" exec_path = self._create_executable() - config_path = self._create_config("INFLUXDB3_OBJECT_STORE=file\n") + config_path = self._create_toml_config('object-store = "file"\n') argv = [ "launcher", @@ -1314,7 +1137,7 @@ def test_invalid_flavor(self): "invalid", "--exec", exec_path, - "--config-env", + "--config-toml", config_path, "--stamp-dir", self.temp_dir, @@ -1335,8 +1158,8 @@ def test_invalid_flavor(self): stderr_output = f.getvalue() self.assertIn("invalid choice", stderr_output.lower()) - def test_missing_both_configs(self): - """Test that missing both config options causes error""" + def test_missing_config_toml(self): + """Test that missing --config-toml causes error""" exec_path = self._create_executable() argv = [ @@ -1368,7 +1191,7 @@ def mock_execve(path, args, env): os.execve = mock_execve exec_path = self._create_executable() - config_path = self._create_config("INFLUXDB3_OBJECT_STORE=file\n") + config_path = self._create_toml_config('object-store = "file"\n') argv = [ "launcher", @@ -1376,7 +1199,7 @@ def mock_execve(path, args, env): "core", "--exec", exec_path, - "--config-env", + "--config-toml", config_path, "--stamp-dir", self.temp_dir, @@ -1407,7 +1230,7 @@ def mock_execve(path, args, env): os.execve = mock_execve exec_path = self._create_executable() - config_path = self._create_config("INFLUXDB3_OBJECT_STORE=file\n") + config_path = self._create_toml_config('object-store = "file"\n') argv = [ "launcher", @@ -1415,7 +1238,7 @@ def mock_execve(path, args, env): "core", "--exec", exec_path, - "--config-env", + "--config-toml", config_path, "--stamp-dir", self.temp_dir, @@ -1442,7 +1265,7 @@ def mock_execve(path, args, env): os.execve = mock_execve exec_path = self._create_executable() - config_path = self._create_config("INFLUXDB3_OBJECT_STORE=file\n") + config_path = self._create_toml_config('object-store = "file"\n') pidfile = os.path.join(self.temp_dir, "test.pid") argv = [ @@ -1451,7 +1274,7 @@ def mock_execve(path, args, env): "core", "--exec", exec_path, - "--config-env", + "--config-toml", config_path, "--stamp-dir", self.temp_dir, @@ -1471,56 +1294,10 @@ def mock_execve(path, args, env): pid = int(f.read().strip()) self.assertEqual(pid, os.getpid()) - def test_config_toml_precedence(self): - """Test that TOML config is used when both configs provided""" - # Mock execve to capture what would be passed - called_with = {} - - def mock_execve(path, args, env): - called_with["path"] = path - called_with["args"] = args - called_with["env"] = env - raise SystemExit(0) - - os.execve = mock_execve - - exec_path = self._create_executable() - toml_path = os.path.join(self.temp_dir, "test.toml") - with open(toml_path, "w") as f: - f.write('object-store = "file"\nnode-id = "from-toml"\n') - - env_path = self._create_config( - "INFLUXDB3_OBJECT_STORE=file\nINFLUXDB3_NODE_IDENTIFIER_PREFIX=from-env\n" - ) - - argv = [ - "launcher", - "--flavor", - "core", - "--exec", - exec_path, - "--config-toml", - toml_path, - "--config-env", - env_path, - "--stamp-dir", - self.temp_dir, - "--", - ] - - with self.assertRaises(SystemExit) as cm: - self.launcher.main(argv) - self.assertEqual(cm.exception.code, 0) - - # Verify TOML config was used, not ENV - self.assertEqual( - called_with["env"]["INFLUXDB3_NODE_IDENTIFIER_PREFIX"], "from-toml" - ) - def test_core_missing_object_store(self): """Test that Core without object-store fails with error""" exec_path = self._create_executable() - config_path = self._create_config("FOO=bar\n") + config_path = self._create_toml_config('foo = "bar"\n') argv = [ "launcher", @@ -1528,7 +1305,7 @@ def test_core_missing_object_store(self): "core", "--exec", exec_path, - "--config-env", + "--config-toml", config_path, "--stamp-dir", self.temp_dir, @@ -1552,7 +1329,7 @@ def test_core_missing_object_store(self): def test_enterprise_missing_license(self): """Test that Enterprise without license fails with error""" exec_path = self._create_executable() - config_path = self._create_config("INFLUXDB3_OBJECT_STORE=file\n") + config_path = self._create_toml_config('object-store = "file"\n') argv = [ "launcher", @@ -1560,7 +1337,7 @@ def test_enterprise_missing_license(self): "enterprise", "--exec", exec_path, - "--config-env", + "--config-toml", config_path, "--stamp-dir", self.temp_dir, @@ -1584,8 +1361,8 @@ def test_enterprise_missing_license(self): def test_enterprise_only_email_fails(self): """Test that Enterprise with only license-email fails""" exec_path = self._create_executable() - config_path = self._create_config( - "INFLUXDB3_OBJECT_STORE=file\nINFLUXDB3_ENTERPRISE_LICENSE_EMAIL=user@example.com\n" + config_path = self._create_toml_config( + 'object-store = "file"\nlicense-email = "user@example.com"\n' ) argv = [ @@ -1594,7 +1371,7 @@ def test_enterprise_only_email_fails(self): "enterprise", "--exec", exec_path, - "--config-env", + "--config-toml", config_path, "--stamp-dir", self.temp_dir, @@ -1628,7 +1405,7 @@ def mock_execve(path, args, env): os.execve = mock_execve exec_path = self._create_executable() - config_path = self._create_config("INFLUXDB3_OBJECT_STORE=file\n") + config_path = self._create_toml_config('object-store = "file"\n') log_file = os.path.join(self.temp_dir, "test.log") argv = [ @@ -1637,7 +1414,7 @@ def mock_execve(path, args, env): "core", "--exec", exec_path, - "--config-env", + "--config-toml", config_path, "--stamp-dir", self.temp_dir, @@ -1664,7 +1441,7 @@ def mock_execve(path, args, env): def test_daemonize_with_pidfile(self): """Test --daemonize with --pidfile creates background process""" exec_path = self._create_executable() - config_path = self._create_config("INFLUXDB3_OBJECT_STORE=file\n") + config_path = self._create_toml_config('object-store = "file"\n') pidfile = os.path.join(self.temp_dir, "daemon.pid") launcher_path = str( Path(__file__).parent / "influxdb3/fs/usr/lib/influxdb3/influxdb3-launcher" @@ -1679,7 +1456,7 @@ def test_daemonize_with_pidfile(self): "core", "--exec", exec_path, - "--config-env", + "--config-toml", config_path, "--stamp-dir", self.temp_dir, @@ -1715,7 +1492,7 @@ def test_daemonize_with_pidfile(self): def test_daemonize_unsupported_platform_via_main(self): """Test that --daemonize fails on unsupported platforms via main()""" exec_path = self._create_executable() - config_path = self._create_config("INFLUXDB3_OBJECT_STORE=file\n") + config_path = self._create_toml_config('object-store = "file"\n') # Temporarily mock PLATFORM_SUPPORTS_FORK to False original_value = getattr(self.launcher, "PLATFORM_SUPPORTS_FORK") @@ -1728,7 +1505,7 @@ def test_daemonize_unsupported_platform_via_main(self): "core", "--exec", exec_path, - "--config-env", + "--config-toml", config_path, "--stamp-dir", self.temp_dir, @@ -1890,9 +1667,9 @@ def setUp(self): def tearDown(self): shutil.rmtree(self.temp_dir, ignore_errors=True) - def _write_config(self, content): - """Helper to write a config file and return its path""" - config_path = os.path.join(self.temp_dir, "test.conf") + def _write_toml_config(self, content): + """Helper to write a TOML config file and return its path""" + config_path = os.path.join(self.temp_dir, "test.toml") with open(config_path, "w", encoding="utf-8") as f: f.write(content) return config_path @@ -1920,15 +1697,15 @@ def _create_mock_executable(self, script_content): return exec_path def test_environment_from_config(self): - """Test that environment variables from config are passed""" - config_path = self._write_config( - "INFLUXDB3_OBJECT_STORE=file\n" "TEST_LAUNCHER_VAR=from_config\n" + """Test that environment variables from TOML config are passed to executed process""" + config_path = self._write_toml_config( + 'object-store = "file"\n' 'node-id = "test-node-123"\n' ) # Create a mock executable that prints an env var mock_exec = self._create_mock_executable( "import os\n" - 'print(f\'TEST_LAUNCHER_VAR={os.environ.get("TEST_LAUNCHER_VAR", "NOT_SET")}\')\n' + 'print(f\'NODE_ID={os.environ.get("INFLUXDB3_NODE_IDENTIFIER_PREFIX", "NOT_SET")}\')\n' ) result = subprocess.run( @@ -1939,7 +1716,7 @@ def test_environment_from_config(self): "core", "--exec", mock_exec, - "--config-env", + "--config-toml", config_path, "--stamp-dir", self.temp_dir, @@ -1949,13 +1726,13 @@ def test_environment_from_config(self): text=True, ) - self.assertIn("TEST_LAUNCHER_VAR=from_config", result.stdout) + self.assertEqual(result.returncode, 0) + self.assertIn("NODE_ID=test-node-123", result.stdout) def test_enterprise_license_file_valid(self): """Test that Enterprise with object-store and license-file works""" - config_path = self._write_config( - "INFLUXDB3_OBJECT_STORE=file\n" - "INFLUXDB3_ENTERPRISE_LICENSE_FILE=/path/to/license.jwt\n" + config_path = self._write_toml_config( + 'object-store = "file"\n' 'license-file = "/path/to/license.jwt"\n' ) # Create a simple mock executable @@ -1969,7 +1746,7 @@ def test_enterprise_license_file_valid(self): "enterprise", "--exec", mock_exec, - "--config-env", + "--config-toml", config_path, "--stamp-dir", self.temp_dir, @@ -1985,7 +1762,7 @@ def test_enterprise_license_file_valid(self): def test_core_with_object_store(self): """Test that Core works with just object-store""" - config_path = self._write_config("INFLUXDB3_OBJECT_STORE=file\n") + config_path = self._write_toml_config('object-store = "file"\n') # Create a simple mock executable mock_exec = self._create_mock_executable("print('Core running')\n") @@ -1998,7 +1775,7 @@ def test_core_with_object_store(self): "core", "--exec", mock_exec, - "--config-env", + "--config-toml", config_path, "--stamp-dir", self.temp_dir, @@ -2015,7 +1792,7 @@ def test_core_with_object_store(self): @unittest.skipIf(os.name == "nt", "Daemonize not available on Windows") def test_daemonize_integration(self): """Integration test for --daemonize with actual forking""" - config_path = self._write_config("INFLUXDB3_OBJECT_STORE=file\n") + config_path = self._write_toml_config('object-store = "file"\n') pidfile = os.path.join(self.temp_dir, "daemon.pid") log_file = os.path.join(self.temp_dir, "daemon.log") marker_file = os.path.join(self.temp_dir, "daemon.marker") @@ -2036,7 +1813,7 @@ def test_daemonize_integration(self): "core", "--exec", mock_exec, - "--config-env", + "--config-toml", config_path, "--stamp-dir", self.temp_dir,