Both deploy paths now take their token values from
config/environments.yml instead of defining their own copies.
CI / shell path:
* scripts/ci/render_subs.py renders deployed files via agebd.subs.
* deploy-and-restart-service.sh drops its dev/prod if-else and its four
sed calls for one render_subs.py invocation, and aborts the deploy if
rendering fails rather than restarting a service whose files still
contain placeholders.
Ansible path:
* ansible/tasks/utils/load-substitutions.yml reads the central config on
the controller and exposes the token table for this environment.
* ioc-install and service-deploy-and-restart now loop over that table
instead of listing tokens inline, so a new token needs no ansible
edit.
* env_suffix_upper/lower are gone from both inventories. The inventory
says which environment it is; the central config says what that
environment means. The facts are renamed to agebd_env_suffix_upper /
_lower to match the token names, removing a rename trap.
Ansible keeps using ansible.builtin.replace rather than calling
agebd.subs, because ioc-install runs on hosts with no uv or Python
environment for this repo. Only the mechanism differs now; the values and
the token list are shared.
Verified: both playbook paths run locally against dev and prod and
produce the expected output, an undeclared environment fails loudly in
both paths, and a misspelled token exits non-zero instead of shipping the
placeholder.
Not addressed here, still step 5: ansible/tasks/utils/get-ioc-port.yml
matches CamelCase registry names against a lowercase input and returns
nothing, so the ansible ioc-install path cannot resolve a port. This is
pre-existing and unchanged by this commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
94 lines
3.3 KiB
Python
94 lines
3.3 KiB
Python
import pytest
|
|
from agebd.subs import SubstitutionError, Substitutions, UnknownTokenError
|
|
from agebd.utils import get_git_root
|
|
|
|
from ci.render_subs import main, render
|
|
|
|
REPO_ROOT = get_git_root(__file__)
|
|
CONFIG_DIR = REPO_ROOT / "config"
|
|
|
|
|
|
@pytest.fixture
|
|
def service_tree(tmp_path):
|
|
ioc = tmp_path / "ioc"
|
|
ioc.mkdir()
|
|
(ioc / "AGEBD-CPCL-TUNE_main.subs").write_text("{ AGEBD-TUNE@@agebd_env_suffix_upper@@ }\n")
|
|
(ioc / "AGEBD-CPCL-TUNE_parameters.yaml").write_text(
|
|
"ioc_host: sls-vserv-bd-01@@agebd_env_suffix_lower@@\nioc_port: @@ioc_port@@\n"
|
|
)
|
|
systemd = tmp_path / "systemd"
|
|
systemd.mkdir()
|
|
(systemd / "AGEBD-SERVICE-TUNE.service").write_text("Environment=AGEBD_ENV=@@agebd_env@@\n")
|
|
return tmp_path
|
|
|
|
|
|
def test_renders_a_whole_service_for_dev(service_tree):
|
|
subs = Substitutions.for_service("dev", "tune", config_dir=CONFIG_DIR)
|
|
|
|
changed = render(subs, [service_tree / "ioc", service_tree / "systemd"])
|
|
|
|
assert len(changed) == 3
|
|
assert "AGEBD-TUNE-DEV" in (service_tree / "ioc/AGEBD-CPCL-TUNE_main.subs").read_text()
|
|
assert (service_tree / "ioc/AGEBD-CPCL-TUNE_parameters.yaml").read_text() == (
|
|
"ioc_host: sls-vserv-bd-01-dev\nioc_port: 50007\n"
|
|
)
|
|
assert (service_tree / "systemd/AGEBD-SERVICE-TUNE.service").read_text() == (
|
|
"Environment=AGEBD_ENV=dev\n"
|
|
)
|
|
|
|
|
|
def test_renders_a_whole_service_for_prod(service_tree):
|
|
subs = Substitutions.for_service("prod", "tune", config_dir=CONFIG_DIR)
|
|
|
|
render(subs, [service_tree / "ioc", service_tree / "systemd"])
|
|
|
|
assert "{ AGEBD-TUNE }" in (service_tree / "ioc/AGEBD-CPCL-TUNE_main.subs").read_text()
|
|
assert "sls-vserv-bd-01\n" in (service_tree / "ioc/AGEBD-CPCL-TUNE_parameters.yaml").read_text()
|
|
|
|
|
|
def test_missing_path_is_skipped_not_fatal(service_tree, capsys):
|
|
subs = Substitutions.for_service("dev", "tune", config_dir=CONFIG_DIR)
|
|
|
|
render(subs, [service_tree / "systemd", service_tree / "does-not-exist"])
|
|
|
|
assert "skipped (does not exist)" in capsys.readouterr().out
|
|
|
|
|
|
def test_leaves_nothing_unrendered(service_tree):
|
|
subs = Substitutions.for_service("dev", "tune", config_dir=CONFIG_DIR)
|
|
|
|
render(subs, [service_tree])
|
|
|
|
for path in service_tree.rglob("*"):
|
|
if path.is_file():
|
|
assert "@@" not in path.read_text(), path
|
|
|
|
|
|
def test_rendering_is_idempotent(service_tree):
|
|
subs = Substitutions.for_service("dev", "tune", config_dir=CONFIG_DIR)
|
|
|
|
assert len(render(subs, [service_tree])) == 3
|
|
assert render(subs, [service_tree]) == []
|
|
|
|
|
|
def test_per_service_token_without_service_fails_loudly(service_tree):
|
|
"""Without --service there is no ioc_port, and that must not pass silently."""
|
|
subs = Substitutions.for_env("dev", config_dir=CONFIG_DIR)
|
|
|
|
with pytest.raises(UnknownTokenError, match="ioc_port"):
|
|
render(subs, [service_tree / "ioc"])
|
|
|
|
|
|
def test_main_renders_via_the_command_line(service_tree):
|
|
exit_code = main(["--env", "dev", "--service", "tune", str(service_tree / "systemd")])
|
|
|
|
assert exit_code == 0
|
|
assert (service_tree / "systemd/AGEBD-SERVICE-TUNE.service").read_text() == (
|
|
"Environment=AGEBD_ENV=dev\n"
|
|
)
|
|
|
|
|
|
def test_main_rejects_an_undeclared_environment(service_tree):
|
|
with pytest.raises(SubstitutionError, match="staging"):
|
|
main(["--env", "staging", "--service", "tune", str(service_tree / "systemd")])
|