Skip to content

Commit 7ccc6ac

Browse files
committed
Move mirror validation to hourly
1 parent ec179f3 commit 7ccc6ac

8 files changed

Lines changed: 141 additions & 32 deletions

File tree

astra_app/core/management/commands/membership_operations.py renamed to astra_app/core/management/commands/operations_daily.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
class Command(BaseCommand):
1111
help = (
12-
"Run the membership cron operations: expiration warnings, expired cleanup, "
12+
"Run the daily operations: expiration warnings, expired cleanup, "
1313
"committee pending-request notifications, and embargoed-members notifications."
1414
)
1515

@@ -32,7 +32,7 @@ def handle(self, *args, **options) -> None:
3232
dry_run: bool = bool(options.get("dry_run"))
3333

3434
logger.info(
35-
"membership_operations: start force=%s dry_run=%s",
35+
"operations_daily: start force=%s dry_run=%s",
3636
force,
3737
dry_run,
3838
)
@@ -43,11 +43,10 @@ def handle(self, *args, **options) -> None:
4343
("freeipa_membership_reconcile", {"report": True, "dry_run": dry_run}),
4444
("membership_pending_requests", {"force": force, "dry_run": dry_run}),
4545
("membership_embargoed_members", {"force": force, "dry_run": dry_run}),
46-
("membership_mirror_validation", {"force": force, "dry_run": dry_run}),
4746
("selfservice_lifecycle_cleanup", {"dry_run": dry_run}),
4847
("account_invitations_refresh", {}),
4948
):
50-
logger.info("membership_operations: running %s", command_name)
49+
logger.info("operations_daily: running %s", command_name)
5150
call_command(command_name, **command_kwargs)
5251

53-
logger.info("membership_operations: complete")
52+
logger.info("operations_daily: complete")
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import logging
2+
from typing import override
3+
4+
from django.core.management import call_command
5+
from django.core.management.base import BaseCommand
6+
7+
logger = logging.getLogger(__name__)
8+
9+
10+
class Command(BaseCommand):
11+
help = "Run the hourly operations: membership mirror validation."
12+
13+
@override
14+
def add_arguments(self, parser) -> None:
15+
parser.add_argument(
16+
"--force",
17+
action="store_true",
18+
help="Pass --force through to sub-commands.",
19+
)
20+
parser.add_argument(
21+
"--dry-run",
22+
action="store_true",
23+
help="Show what would be done without mutating data or sending email.",
24+
)
25+
26+
@override
27+
def handle(self, *args, **options) -> None:
28+
force: bool = bool(options.get("force"))
29+
dry_run: bool = bool(options.get("dry_run"))
30+
31+
logger.info(
32+
"operations_hourly: start force=%s dry_run=%s",
33+
force,
34+
dry_run,
35+
)
36+
37+
for command_name, command_kwargs in (
38+
("membership_mirror_validation", {"force": force, "dry_run": dry_run}),
39+
):
40+
logger.info("operations_hourly: running %s", command_name)
41+
call_command(command_name, **command_kwargs)
42+
43+
logger.info("operations_hourly: complete")
Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
1-
21
from unittest.mock import call, patch
32

43
from django.core.management import call_command
54
from django.test import TestCase
65

76

8-
class MembershipOperationsCommandTests(TestCase):
9-
def test_command_runs_all_membership_jobs(self) -> None:
7+
class OperationsDailyCommandTests(TestCase):
8+
def test_command_runs_daily_jobs(self) -> None:
109
with (
11-
patch("core.management.commands.membership_operations.call_command") as cc,
12-
self.assertLogs("core.management.commands.membership_operations", level="INFO") as logs,
10+
patch("core.management.commands.operations_daily.call_command") as cc,
11+
self.assertLogs("core.management.commands.operations_daily", level="INFO") as logs,
1312
):
14-
call_command("membership_operations")
13+
call_command("operations_daily")
1514

1615
self.assertEqual(
1716
cc.mock_calls,
@@ -21,21 +20,20 @@ def test_command_runs_all_membership_jobs(self) -> None:
2120
call("freeipa_membership_reconcile", report=True, dry_run=False),
2221
call("membership_pending_requests", force=False, dry_run=False),
2322
call("membership_embargoed_members", force=False, dry_run=False),
24-
call("membership_mirror_validation", force=False, dry_run=False),
2523
call("selfservice_lifecycle_cleanup", dry_run=False),
2624
call("account_invitations_refresh"),
2725
],
2826
)
2927
self.assertTrue(
30-
any("membership_operations" in line for line in logs.output),
31-
f"Expected membership operations logs, got: {logs.output}",
28+
any("operations_daily" in line for line in logs.output),
29+
f"Expected daily operations logs, got: {logs.output}",
3230
)
3331

3432
def test_force_is_passed_through(self) -> None:
3533
with patch(
36-
"core.management.commands.membership_operations.call_command",
34+
"core.management.commands.operations_daily.call_command",
3735
) as cc:
38-
call_command("membership_operations", "--force")
36+
call_command("operations_daily", "--force")
3937

4038
self.assertEqual(
4139
cc.mock_calls,
@@ -45,17 +43,16 @@ def test_force_is_passed_through(self) -> None:
4543
call("freeipa_membership_reconcile", report=True, dry_run=False),
4644
call("membership_pending_requests", force=True, dry_run=False),
4745
call("membership_embargoed_members", force=True, dry_run=False),
48-
call("membership_mirror_validation", force=True, dry_run=False),
4946
call("selfservice_lifecycle_cleanup", dry_run=False),
5047
call("account_invitations_refresh"),
5148
],
5249
)
5350

5451
def test_dry_run_is_passed_through(self) -> None:
5552
with patch(
56-
"core.management.commands.membership_operations.call_command",
53+
"core.management.commands.operations_daily.call_command",
5754
) as cc:
58-
call_command("membership_operations", "--dry-run")
55+
call_command("operations_daily", "--dry-run")
5956

6057
self.assertEqual(
6158
cc.mock_calls,
@@ -65,8 +62,53 @@ def test_dry_run_is_passed_through(self) -> None:
6562
call("freeipa_membership_reconcile", report=True, dry_run=True),
6663
call("membership_pending_requests", force=False, dry_run=True),
6764
call("membership_embargoed_members", force=False, dry_run=True),
68-
call("membership_mirror_validation", force=False, dry_run=True),
6965
call("selfservice_lifecycle_cleanup", dry_run=True),
7066
call("account_invitations_refresh"),
7167
],
7268
)
69+
70+
71+
class OperationsHourlyCommandTests(TestCase):
72+
def test_command_runs_hourly_jobs(self) -> None:
73+
with (
74+
patch("core.management.commands.operations_hourly.call_command") as cc,
75+
self.assertLogs("core.management.commands.operations_hourly", level="INFO") as logs,
76+
):
77+
call_command("operations_hourly")
78+
79+
self.assertEqual(
80+
cc.mock_calls,
81+
[
82+
call("membership_mirror_validation", force=False, dry_run=False),
83+
],
84+
)
85+
self.assertTrue(
86+
any("operations_hourly" in line for line in logs.output),
87+
f"Expected hourly operations logs, got: {logs.output}",
88+
)
89+
90+
def test_force_is_passed_through(self) -> None:
91+
with patch(
92+
"core.management.commands.operations_hourly.call_command",
93+
) as cc:
94+
call_command("operations_hourly", "--force")
95+
96+
self.assertEqual(
97+
cc.mock_calls,
98+
[
99+
call("membership_mirror_validation", force=True, dry_run=False),
100+
],
101+
)
102+
103+
def test_dry_run_is_passed_through(self) -> None:
104+
with patch(
105+
"core.management.commands.operations_hourly.call_command",
106+
) as cc:
107+
call_command("operations_hourly", "--dry-run")
108+
109+
self.assertEqual(
110+
cc.mock_calls,
111+
[
112+
call("membership_mirror_validation", force=False, dry_run=True),
113+
],
114+
)

infra/README.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,15 +103,20 @@ Define cron jobs in Terraform using the `cron_jobs` variable. Example:
103103
```hcl
104104
cron_jobs = [
105105
{
106-
name = "membership-operations"
106+
name = "operations-hourly"
107+
minute = "0"
108+
command = "podman exec astra-app-1 python manage.py operations_hourly"
109+
},
110+
{
111+
name = "operations-daily"
107112
minute = "0"
108113
hour = "0"
109-
command = "podman exec astra-app-1 python manage.py membership_operations"
114+
command = "podman exec astra-app-1 python manage.py operations_daily"
110115
}
111116
]
112117
```
113118

114119
If `minute` or `hour` are omitted, they default to `0` (midnight local time).
115120

116-
The membership operations job now includes FreeIPA membership reconciliation in report mode by default and alerts
117-
members of the configured `FREEIPA_ADMIN_GROUP` when drift or failures are detected.
121+
The daily operations job includes FreeIPA membership reconciliation in report mode by default and alerts members
122+
of the configured `FREEIPA_ADMIN_GROUP` when drift or failures are detected.

infra/envs/prod/terraform.tfvars.example

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,14 @@ django_migrate_retries = 30
5555
# --- Cron jobs on the host
5656
# cron_jobs = [
5757
# {
58-
# name = "membership-operations"
58+
# name = "operations-hourly"
59+
# minute = "0"
60+
# command = "podman exec astra-app-1 python manage.py operations_hourly"
61+
# },
62+
# {
63+
# name = "operations-daily"
5964
# minute = "0"
6065
# hour = "0"
61-
# command = "podman exec astra-app-1 python manage.py membership_operations"
66+
# command = "podman exec astra-app-1 python manage.py operations_daily"
6267
# }
6368
# ]

infra/envs/prod/variables.tf

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,10 +195,15 @@ variable "cron_jobs" {
195195
description = "Cron jobs to configure on the host."
196196
default = [
197197
{
198-
name = "membership-operations"
198+
name = "operations-hourly"
199+
minute = "0"
200+
command = "podman exec astra-app-1 python manage.py operations_hourly"
201+
},
202+
{
203+
name = "operations-daily"
199204
minute = "0"
200205
hour = "0"
201-
command = "podman exec astra-app-1 python manage.py membership_operations"
206+
command = "podman exec astra-app-1 python manage.py operations_daily"
202207
}
203208
]
204209
}

infra/envs/staging/terraform.tfvars.example

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,14 @@ django_migrate_retries = 30
6363
# --- Cron jobs on the host
6464
# cron_jobs = [
6565
# {
66-
# name = "membership-operations"
66+
# name = "operations-hourly"
67+
# minute = "0"
68+
# command = "podman exec astra-app-1 python manage.py operations_hourly"
69+
# },
70+
# {
71+
# name = "operations-daily"
6772
# minute = "0"
6873
# hour = "0"
69-
# command = "podman exec astra-app-1 python manage.py membership_operations"
74+
# command = "podman exec astra-app-1 python manage.py operations_daily"
7075
# }
7176
# ]

infra/envs/staging/variables.tf

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,10 +224,15 @@ variable "cron_jobs" {
224224
description = "Cron jobs to configure on the host."
225225
default = [
226226
{
227-
name = "membership-operations"
227+
name = "operations-hourly"
228+
minute = "0"
229+
command = "podman exec astra-app-1 python manage.py operations_hourly"
230+
},
231+
{
232+
name = "operations-daily"
228233
minute = "0"
229234
hour = "0"
230-
command = "podman exec astra-app-1 python manage.py membership_operations"
235+
command = "podman exec astra-app-1 python manage.py operations_daily"
231236
}
232237
]
233238
}

0 commit comments

Comments
 (0)