Account for deferred retirements in apply budget
This commit is contained in:
+14
-3
@@ -65,7 +65,7 @@ def build_parser():
|
|||||||
"--max-changes",
|
"--max-changes",
|
||||||
type=_nonnegative_int,
|
type=_nonnegative_int,
|
||||||
default=None,
|
default=None,
|
||||||
help="Maximum CREATE/UPDATE actions allowed with --apply",
|
help="Maximum planned mutations allowed with --apply",
|
||||||
)
|
)
|
||||||
|
|
||||||
sub.add_parser("plugins", help="Show the configured source/provider plugins")
|
sub.add_parser("plugins", help="Show the configured source/provider plugins")
|
||||||
@@ -202,11 +202,21 @@ def run_reconcile(args):
|
|||||||
for action in plan.actions
|
for action in plan.actions
|
||||||
)
|
)
|
||||||
|
|
||||||
|
retirement_count = sum(
|
||||||
|
len(plan.retirements)
|
||||||
|
for plan in result.plans
|
||||||
|
)
|
||||||
|
mutation_count = (
|
||||||
|
change_count
|
||||||
|
+ retirement_count
|
||||||
|
)
|
||||||
|
|
||||||
print()
|
print()
|
||||||
print(
|
print(
|
||||||
"Plan summary: "
|
"Plan summary: "
|
||||||
f"{create_count} create, "
|
f"{create_count} create, "
|
||||||
f"{update_count} update, "
|
f"{update_count} update, "
|
||||||
|
f"{retirement_count} retirement, "
|
||||||
f"{noop_count} unchanged"
|
f"{noop_count} unchanged"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -221,10 +231,10 @@ def run_reconcile(args):
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
args.max_changes is not None
|
args.max_changes is not None
|
||||||
and change_count > args.max_changes
|
and mutation_count > args.max_changes
|
||||||
):
|
):
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Refusing to apply {change_count} change(s); "
|
f"Refusing to apply {mutation_count} mutation(s); "
|
||||||
f"--max-changes allows {args.max_changes}"
|
f"--max-changes allows {args.max_changes}"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -241,6 +251,7 @@ def run_reconcile(args):
|
|||||||
print(
|
print(
|
||||||
"APPLY COMPLETE: "
|
"APPLY COMPLETE: "
|
||||||
f"{len(execution.applied)} action(s) applied and verified; "
|
f"{len(execution.applied)} action(s) applied and verified; "
|
||||||
|
f"{len(execution.retired)} retirement(s) completed; "
|
||||||
f"{len(execution.skipped)} unchanged/skipped."
|
f"{len(execution.skipped)} unchanged/skipped."
|
||||||
)
|
)
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class ExecutedAction:
|
|||||||
class ExecutionResult:
|
class ExecutionResult:
|
||||||
applied: tuple[ExecutedAction, ...]
|
applied: tuple[ExecutedAction, ...]
|
||||||
skipped: tuple[PlanAction, ...]
|
skipped: tuple[PlanAction, ...]
|
||||||
|
retired: tuple[tuple[str, RetireRequest], ...]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -555,6 +556,9 @@ def execute_endpoint_plans(
|
|||||||
retired_identities: set[
|
retired_identities: set[
|
||||||
tuple[str, str, str]
|
tuple[str, str, str]
|
||||||
] = set()
|
] = set()
|
||||||
|
retired_requests: list[
|
||||||
|
tuple[str, RetireRequest]
|
||||||
|
] = []
|
||||||
|
|
||||||
for endpoint_id, retirement in pending_retirements:
|
for endpoint_id, retirement in pending_retirements:
|
||||||
retired = retirement.object
|
retired = retirement.object
|
||||||
@@ -580,8 +584,15 @@ def execute_endpoint_plans(
|
|||||||
)
|
)
|
||||||
|
|
||||||
retire(retirement)
|
retire(retirement)
|
||||||
|
retired_requests.append(
|
||||||
|
(
|
||||||
|
endpoint_id,
|
||||||
|
retirement,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return ExecutionResult(
|
return ExecutionResult(
|
||||||
applied=tuple(applied),
|
applied=tuple(applied),
|
||||||
skipped=tuple(skipped),
|
skipped=tuple(skipped),
|
||||||
|
retired=tuple(retired_requests),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -220,6 +220,7 @@ class ReconcileCliTests(unittest.TestCase):
|
|||||||
execution = SimpleNamespace(
|
execution = SimpleNamespace(
|
||||||
applied=(),
|
applied=(),
|
||||||
skipped=(),
|
skipped=(),
|
||||||
|
retired=(),
|
||||||
)
|
)
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
@@ -255,11 +256,14 @@ class ReconcileCliTests(unittest.TestCase):
|
|||||||
stdout.getvalue(),
|
stdout.getvalue(),
|
||||||
)
|
)
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
"APPLY COMPLETE: 0 action(s) applied and verified",
|
"APPLY COMPLETE: 0 action(s) applied and verified; "
|
||||||
|
"0 retirement(s) completed",
|
||||||
stdout.getvalue(),
|
stdout.getvalue(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_apply_budget_blocks_executor_before_mutation(self):
|
def test_apply_budget_counts_planned_retirement_before_mutation(
|
||||||
|
self,
|
||||||
|
):
|
||||||
config = SimpleNamespace(
|
config = SimpleNamespace(
|
||||||
target_ip="192.168.1.101",
|
target_ip="192.168.1.101",
|
||||||
include_disabled=False,
|
include_disabled=False,
|
||||||
@@ -267,10 +271,18 @@ class ReconcileCliTests(unittest.TestCase):
|
|||||||
domain=None,
|
domain=None,
|
||||||
)
|
)
|
||||||
adapter = object()
|
adapter = object()
|
||||||
action = SimpleNamespace(
|
|
||||||
operation=SimpleNamespace(value="create"),
|
retirement = SimpleNamespace(
|
||||||
|
object=SimpleNamespace(
|
||||||
|
object_type="certificates",
|
||||||
|
logical_key="app.example.com",
|
||||||
|
local_key="63",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
plan = SimpleNamespace(
|
||||||
|
actions=(),
|
||||||
|
retirements=(retirement,),
|
||||||
)
|
)
|
||||||
plan = SimpleNamespace(actions=(action,))
|
|
||||||
reconciliation = SimpleNamespace(
|
reconciliation = SimpleNamespace(
|
||||||
observations=(),
|
observations=(),
|
||||||
desired=(),
|
desired=(),
|
||||||
@@ -298,7 +310,64 @@ class ReconcileCliTests(unittest.TestCase):
|
|||||||
with self.assertRaisesRegex(
|
with self.assertRaisesRegex(
|
||||||
RuntimeError,
|
RuntimeError,
|
||||||
(
|
(
|
||||||
r"Refusing to apply 1 change\(s\); "
|
r"Refusing to apply 1 mutation\(s\); "
|
||||||
|
r"--max-changes allows 0"
|
||||||
|
),
|
||||||
|
):
|
||||||
|
run_reconcile(
|
||||||
|
SimpleNamespace(
|
||||||
|
state_db=str(state_path),
|
||||||
|
include_disabled=False,
|
||||||
|
apply=True,
|
||||||
|
max_changes=0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
execute.assert_not_called()
|
||||||
|
|
||||||
|
def test_apply_budget_blocks_executor_before_mutation(self):
|
||||||
|
config = SimpleNamespace(
|
||||||
|
target_ip="192.168.1.101",
|
||||||
|
include_disabled=False,
|
||||||
|
domain_matching=False,
|
||||||
|
domain=None,
|
||||||
|
)
|
||||||
|
adapter = object()
|
||||||
|
action = SimpleNamespace(
|
||||||
|
operation=SimpleNamespace(value="create"),
|
||||||
|
)
|
||||||
|
plan = SimpleNamespace(
|
||||||
|
actions=(action,),
|
||||||
|
retirements=(),
|
||||||
|
)
|
||||||
|
reconciliation = SimpleNamespace(
|
||||||
|
observations=(),
|
||||||
|
desired=(),
|
||||||
|
plans=(plan,),
|
||||||
|
failures=(),
|
||||||
|
)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
state_path = Path(directory) / "state.sqlite3"
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"dns_sync.cli.AppConfig.from_env",
|
||||||
|
return_value=config,
|
||||||
|
), patch(
|
||||||
|
"dns_sync.cli._configured_adapters",
|
||||||
|
return_value=[adapter],
|
||||||
|
), patch(
|
||||||
|
"dns_sync.cli.discover_reconcile_plan",
|
||||||
|
return_value=reconciliation,
|
||||||
|
), patch(
|
||||||
|
"dns_sync.cli.print_endpoint_plans",
|
||||||
|
), patch(
|
||||||
|
"dns_sync.cli.execute_endpoint_plans",
|
||||||
|
) as execute, redirect_stdout(io.StringIO()):
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
RuntimeError,
|
||||||
|
(
|
||||||
|
r"Refusing to apply 1 mutation\(s\); "
|
||||||
r"--max-changes allows 0"
|
r"--max-changes allows 0"
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
|
|||||||
+12
-1
@@ -553,7 +553,7 @@ class ExecutionTests(unittest.TestCase):
|
|||||||
NOW,
|
NOW,
|
||||||
)
|
)
|
||||||
|
|
||||||
execute_endpoint_plans(
|
result = execute_endpoint_plans(
|
||||||
[adapter],
|
[adapter],
|
||||||
[plan],
|
[plan],
|
||||||
store,
|
store,
|
||||||
@@ -590,6 +590,17 @@ class ExecutionTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
result.retired,
|
||||||
|
(
|
||||||
|
(
|
||||||
|
endpoint.identity,
|
||||||
|
RetireRequest(
|
||||||
|
object=old_certificate,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
def test_failed_verification_is_persisted_and_raises(self):
|
def test_failed_verification_is_persisted_and_raises(self):
|
||||||
endpoint = Endpoint("verizon", "192.168.1.1:443")
|
endpoint = Endpoint("verizon", "192.168.1.1:443")
|
||||||
|
|||||||
Reference in New Issue
Block a user