fix(migrate): auto-fold a unit-less default when every secondary shares one manager
The service-domain guard compared each standalone secondary against the DEFAULT profile's installed units. A default that never had a gateway unit (the #118097 fleet: N launchd profiles, one macOS user, no unit on default) therefore produced one "different service domain" blocker per secondary, and maybe_auto_migrate_after_update printed blockers instead of folding — on the most common upgrade shape. The reference is now the manager the plan already elects as the target (MigrationPlan.target_service_kind: the default's own unit, else the one the secondaries use), so the guard agrees with the apply path. Genuinely mixed topologies still refuse: a default unit under another manager than a secondary, two managers among the secondaries (the non-elected one differs from the target), user-vs-system systemd, >1 unit on a profile. Test: red on the PR head (base: one finding per launchd secondary), green now; both controls asserted in the same test.
This commit is contained in:
@@ -91,16 +91,25 @@ def _service_label(profile: ProfileGateway) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _guard_service_domain(plan: MigrationPlan, profile: ProfileGateway) -> Optional[str]:
|
def _guard_service_domain(plan: MigrationPlan, profile: ProfileGateway) -> Optional[str]:
|
||||||
"""Different manager or scope than the default gateway (system vs user systemd, launchd vs systemd,
|
"""Different manager or scope than the one the fleet converges on (system vs user systemd, launchd vs
|
||||||
or any service when the default is detached: the auto path never elects a secondary's manager).
|
systemd). The reference is the default's own unit when it has one, else the manager
|
||||||
Two units on one profile is an ambiguous topology the unattended path does not resolve either."""
|
``target_service_kind()`` elects from the secondaries: a default that never had a gateway unit is not a
|
||||||
|
service domain of its own, and refusing every secondary against it left the common upgrade fleet (N
|
||||||
|
launchd profiles, unit-less default, #118097) printing blockers instead of folding. Two managers
|
||||||
|
among the secondaries still refuse — the ones not elected differ from the target. Two units on one
|
||||||
|
profile is an ambiguous topology the unattended path does not resolve either."""
|
||||||
if len(profile.services) > 1:
|
if len(profile.services) > 1:
|
||||||
return (f"Profile '{profile.name}' has more than one installed service ({profile.service_label()}): "
|
return (f"Profile '{profile.name}' has more than one installed service ({profile.service_label()}): "
|
||||||
f"an ambiguous service topology is not folded automatically.")
|
f"an ambiguous service topology is not folded automatically.")
|
||||||
if set(profile.services) == set(plan.default.services):
|
target = plan.target_service_kind()
|
||||||
|
reference = plan.default.services or ([target] if target is not None else [])
|
||||||
|
if set(profile.services) == set(reference):
|
||||||
return None
|
return None
|
||||||
return (f"Profile '{profile.name}' runs under {_service_label(profile)} while the default gateway "
|
from hermes_cli.gateway_migrate import _service_label as _kind_label
|
||||||
f"runs under {_service_label(plan.default)}: a different service domain is not folded automatically.")
|
against = (f"the default gateway runs under {_service_label(plan.default)}" if plan.default.services
|
||||||
|
else f"the fleet converges on {_kind_label(target)}")
|
||||||
|
return (f"Profile '{profile.name}' runs under {_service_label(profile)} while {against}: "
|
||||||
|
f"a different service domain is not folded automatically.")
|
||||||
|
|
||||||
|
|
||||||
def _guard_unix_user(plan: MigrationPlan, profile: ProfileGateway) -> Optional[str]:
|
def _guard_unix_user(plan: MigrationPlan, profile: ProfileGateway) -> Optional[str]:
|
||||||
|
|||||||
@@ -373,6 +373,38 @@ def test_update_hook_still_migrates_same_user_same_scope_profiles_under_the_defa
|
|||||||
assert _config_flag(fleet.root) is True and ("ops", "uninstall") in fleet.ops
|
assert _config_flag(fleet.root) is True and ("ops", "uninstall") in fleet.ops
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_hook_folds_a_unit_less_default_when_every_secondary_shares_one_manager(fleet, capsys, monkeypatch):
|
||||||
|
"""The #118097 fleet: the default profile never had a gateway unit, every secondary runs under the
|
||||||
|
SAME manager (all launchd, one macOS user). That is one service domain, and the manager the plan
|
||||||
|
elects as the target (``target_service_kind``) is the reference the guard must agree with — not the
|
||||||
|
default's empty unit list, which turned every such fleet into "blockers" instead of a fold.
|
||||||
|
Controls: a default unit under another manager, and two managers among the secondaries, still refuse."""
|
||||||
|
from hermes_cli.gateway_migrate_guards import auto_migration_blockers
|
||||||
|
monkeypatch.setattr(gm, "_gateway_identity", lambda home, pid, service: (1000, home), raising=False)
|
||||||
|
fleet.services.update({"coder": ("launchd", False), "ops": ("launchd", False)})
|
||||||
|
assert "default" not in fleet.services
|
||||||
|
plan = gm.build_migration_plan()
|
||||||
|
assert plan.target_service_kind() == ("launchd", False)
|
||||||
|
assert auto_migration_blockers(plan) == []
|
||||||
|
|
||||||
|
gm.maybe_auto_migrate_after_update()
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "serves 3 profiles" in out and _config_flag(fleet.root) is True
|
||||||
|
assert ("coder", "uninstall") in fleet.ops and ("ops", "uninstall") in fleet.ops
|
||||||
|
assert ("default", "install") in fleet.ops and fleet.services == {"default": ("launchd", False)}
|
||||||
|
|
||||||
|
# Control 1: the default HAS a unit under another manager -> still a different service domain.
|
||||||
|
fleet.services.update({"default": ("systemd", False), "coder": ("launchd", False), "ops": ("launchd", False)})
|
||||||
|
fleet.pids.update({"coder": 4101, "ops": 4102})
|
||||||
|
blockers = auto_migration_blockers(gm.build_migration_plan())
|
||||||
|
assert blockers and all("different service domain" in b for b in blockers)
|
||||||
|
# Control 2: unit-less default, secondaries under two managers -> the non-elected one refuses.
|
||||||
|
fleet.services.pop("default")
|
||||||
|
fleet.services.update({"coder": ("launchd", False), "ops": ("systemd", False)})
|
||||||
|
blockers = auto_migration_blockers(gm.build_migration_plan())
|
||||||
|
assert len(blockers) == 1 and "'ops'" in blockers[0] and "different service domain" in blockers[0]
|
||||||
|
|
||||||
|
|
||||||
def test_auto_multiplex_migration_false_opts_out_of_the_update_hook_but_not_the_explicit_command(fleet, capsys):
|
def test_auto_multiplex_migration_false_opts_out_of_the_update_hook_but_not_the_explicit_command(fleet, capsys):
|
||||||
"""``gateway.auto_multiplex_migration: false`` is a durable opt-out: an otherwise-eligible fleet is
|
"""``gateway.auto_multiplex_migration: false`` is a durable opt-out: an otherwise-eligible fleet is
|
||||||
left alone by ``hermes update`` (no output, no ops, no flag flip), while the operator typing
|
left alone by ``hermes update`` (no output, no ops, no flag flip), while the operator typing
|
||||||
|
|||||||
Reference in New Issue
Block a user