mirror of
https://github.com/Sea-Haven-Industries/afterhours-shift-manager.git
synced 2026-09-30 07:53:11 +00:00
Local CI and the image deploy duplicated the org workflows and still required ci / ci. Pin the callers to those workflows and trust the reusable deploy ref.
149 lines
4.9 KiB
Python
149 lines
4.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Recreate future holiday-* EventBridge Scheduler schedules in prod.
|
|
|
|
Reads outstanding holiday-activate-* / holiday-deactivate-* from mgmt and
|
|
creates the same names in prod targeting the prod router ARN and scheduler
|
|
role. Dry-run unless --execute.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import re
|
|
import sys
|
|
from datetime import datetime, timezone
|
|
from zoneinfo import ZoneInfo
|
|
|
|
import boto3
|
|
from botocore.exceptions import ClientError
|
|
|
|
SRC_ACCOUNT = "328440206208"
|
|
DST_ACCOUNT = "011934824531"
|
|
PROD_ROUTER_ARN = (
|
|
"arn:aws:lambda:us-east-1:011934824531:function:afterhours-holiday-router"
|
|
)
|
|
PROD_ROLE_ARN = "arn:aws:iam::011934824531:role/tf-managed/afterhours-shift-manager-holiday-scheduler"
|
|
PREFIXES = ("holiday-activate-", "holiday-deactivate-")
|
|
_AT = re.compile(r"^at\((\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})\)$")
|
|
|
|
|
|
def _client(profile: str, region: str):
|
|
return boto3.Session(profile_name=profile, region_name=region).client("scheduler")
|
|
|
|
|
|
def _account(profile: str) -> str:
|
|
return (
|
|
boto3.Session(profile_name=profile)
|
|
.client("sts")
|
|
.get_caller_identity()["Account"]
|
|
)
|
|
|
|
|
|
def schedule_when(detail: dict) -> datetime | None:
|
|
"""UTC instant the one-off schedule fires, or None if it cannot be parsed."""
|
|
expr = (detail.get("ScheduleExpression") or "").strip()
|
|
tzname = detail.get("ScheduleExpressionTimezone") or "America/New_York"
|
|
match = _AT.match(expr)
|
|
if match:
|
|
naive = datetime.strptime(match.group(1), "%Y-%m-%dT%H:%M:%S")
|
|
return naive.replace(tzinfo=ZoneInfo(tzname)).astimezone(timezone.utc)
|
|
at = detail.get("EndDate") or detail.get("StartDate")
|
|
if at is None:
|
|
return None
|
|
if at.tzinfo is None:
|
|
return at.replace(tzinfo=timezone.utc)
|
|
return at.astimezone(timezone.utc)
|
|
|
|
|
|
def _list_holiday(client):
|
|
names = []
|
|
token = None
|
|
while True:
|
|
kwargs = {"GroupName": "default"}
|
|
if token:
|
|
kwargs["NextToken"] = token
|
|
resp = client.list_schedules(**kwargs)
|
|
for item in resp.get("Schedules", []):
|
|
name = item.get("Name", "")
|
|
if name.startswith(PREFIXES):
|
|
names.append(name)
|
|
token = resp.get("NextToken")
|
|
if not token:
|
|
return names
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--src-profile", required=True)
|
|
parser.add_argument("--dst-profile", required=True)
|
|
parser.add_argument("--region", default="us-east-1")
|
|
parser.add_argument("--execute", action="store_true")
|
|
args = parser.parse_args()
|
|
|
|
if _account(args.src_profile) != SRC_ACCOUNT:
|
|
print("src profile is not mgmt", file=sys.stderr)
|
|
return 2
|
|
if _account(args.dst_profile) != DST_ACCOUNT:
|
|
print("dst profile is not prod", file=sys.stderr)
|
|
return 2
|
|
|
|
src = _client(args.src_profile, args.region)
|
|
dst = _client(args.dst_profile, args.region)
|
|
now = datetime.now(timezone.utc)
|
|
created = 0
|
|
skipped = 0
|
|
failed = 0
|
|
|
|
for name in _list_holiday(src):
|
|
detail = src.get_schedule(Name=name, GroupName="default")
|
|
expr = detail.get("ScheduleExpression", "")
|
|
tzname = detail.get("ScheduleExpressionTimezone", "America/New_York")
|
|
when = schedule_when(detail)
|
|
if when is not None and when < now:
|
|
print(f"skip past {name} expr={expr}")
|
|
skipped += 1
|
|
continue
|
|
payload = {
|
|
"Name": name,
|
|
"GroupName": "default",
|
|
"ScheduleExpression": expr,
|
|
"ScheduleExpressionTimezone": tzname,
|
|
"FlexibleTimeWindow": {"Mode": "OFF"},
|
|
"Target": {
|
|
"Arn": PROD_ROUTER_ARN,
|
|
"RoleArn": PROD_ROLE_ARN,
|
|
"Input": detail.get("Target", {}).get("Input", ""),
|
|
},
|
|
"ActionAfterCompletion": detail.get("ActionAfterCompletion", "DELETE"),
|
|
}
|
|
if detail.get("EndDate"):
|
|
payload["EndDate"] = detail["EndDate"]
|
|
print(f"would create {name} expr={expr} tz={tzname}")
|
|
if not args.execute:
|
|
continue
|
|
try:
|
|
dst.create_schedule(**payload)
|
|
created += 1
|
|
except ClientError as exc:
|
|
code = exc.response["Error"]["Code"]
|
|
if code == "ConflictException":
|
|
print(f"exists {name}")
|
|
elif code == "ValidationException":
|
|
print(
|
|
f"skip invalid {name}: {exc.response['Error'].get('Message', code)}"
|
|
)
|
|
skipped += 1
|
|
else:
|
|
print(f"failed {name}: {code}", file=sys.stderr)
|
|
failed += 1
|
|
|
|
print(
|
|
f"created={created} skipped_past={skipped} failed={failed} execute={args.execute}"
|
|
)
|
|
if not args.execute:
|
|
print("dry-run; pass --execute to CreateSchedule")
|
|
return 1 if failed else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|