Skip to content

Commit

Permalink
WIP; [2/2] Announce maintenance in stable deployments (#5979)
Browse files Browse the repository at this point in the history
  • Loading branch information
achave11-ucsc committed Mar 26, 2024
1 parent ee2be8d commit 393914b
Show file tree
Hide file tree
Showing 3 changed files with 171 additions and 8 deletions.
17 changes: 17 additions & 0 deletions lambdas/service/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@
from azul.logging import (
configure_app_logging,
)
from azul.maintenance import (
MaintenanceService,
)
from azul.openapi import (
application_json,
format_description as fd,
Expand Down Expand Up @@ -324,6 +327,10 @@ def manifest_controller(self) -> ManifestController:
return self._service_controller(ManifestController,
manifest_url_func=manifest_url_func)

@cached_property
def maintenance_service(self) -> MaintenanceService:
return MaintenanceService()

def _service_controller(self, controller_cls: Type[C], **kwargs) -> C:
file_url_func: FileUrlFunc = self.file_url
return self._controller(controller_cls,
Expand Down Expand Up @@ -839,6 +846,16 @@ def get_integrations():
body=json.dumps(body))


@app.route(
'/maintenance/schedule',
methods=['GET'],
cors=True
# TODO: spect-out
)
def get_maintenance():
return app.maintenance_service.list_upcoming_events()


@app.route(
'/index/catalogs',
methods=['GET'],
Expand Down
84 changes: 84 additions & 0 deletions scripts/manage_maintenance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import argparse
import json

from azul.maintenance import (
MaintenanceService,
MaintenanceEvent
)

from datetime import datetime, timedelta


def parse_duration(duration_str):
# TODO, Code to parse the duration string (e.g., "1h30m", "2d")
# and return a timedelta object
return 'TODO'


def main():
parser = argparse.ArgumentParser(description="Maintenance Schedule CLI")
subparsers = parser.add_subparsers(dest="command")

list_parser = subparsers.add_parser("list", help="List events in JSON form")
list_parser.add_argument("--all", action="store_true",
help="Include completed events")

add_parser = subparsers.add_parser("add", help="Schedule an event")
add_parser.add_argument("--start", required=True,
help="Event start time (ISO format)")
add_parser.add_argument("--duration", required=True,
help="Event duration (e.g., '1h30m', '2d')")
add_parser.add_argument("--description", required=True,
help="Event description")
add_parser.add_argument("--partial-responses", nargs="+",
help="Catalog names for partial responses")
add_parser.add_argument("--degraded-performance", nargs="+",
help="Catalog names for degraded performance")
add_parser.add_argument("--service-unavailable", nargs="+",
help="Catalog names for service unavailability")

cancel_parser = subparsers.add_parser("cancel",
help="Cancel a pending event")
cancel_parser.add_argument("--index", type=int, required=True,
help="Index of the event to cancel")

start_parser = subparsers.add_parser("start",
help="Activate a pending event")
start_parser.add_argument("--index", type=int, required=True,
help="Index of the event to start")

end_parser = subparsers.add_parser("end", help="Complete the active event")

Check notice

Code scanning / CodeQL

Unused local variable Note

Variable end_parser is not used.

adjust_parser = subparsers.add_parser("adjust",
help="Modify the active event")
adjust_parser.add_argument("--duration", required=True,
help="New event duration (e.g., '1h30m', '2d')")

args = parser.parse_args()

event_service = MaintenanceService()

if args.command == "list":
events = event_service.list_upcoming_events(args.all)
print(json.dumps(events, indent=4))

elif args.command == "add":
event = MaintenanceEvent.from_args(args)
event_service.add_event(event)

elif args.command == "cancel":
event_service.cancel_event(args.index)

elif args.command == "start":
event_service.start_event(args.index)

elif args.command == "end":
event_service.end_event()

elif args.command == "adjust":
new_duration = parse_duration(args.duration)
event_service.adjust_event(new_duration)


if __name__ == "__main__":
main()
78 changes: 70 additions & 8 deletions src/azul/maintenance.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
from operator import (
attrgetter,
)
import sys
from typing import (
Iterator,
Sequence,
Self,
)

import attrs
Expand All @@ -27,10 +27,19 @@
JSON,
reject,
require,
CatalogName,
cached_property,
config,
)
from azul.collections import (
adict,
)
from azul.deployment import (
aws,
)
from azul.service.storage_service import (
StorageService,
)
from azul.time import (
format_dcp2_datetime,
parse_dcp2_datetime,
Expand All @@ -46,7 +55,7 @@ class MaintenanceImpactKind(Enum):
@attrs.define
class MaintenanceImpact:
kind: MaintenanceImpactKind
affected_catalogs: list[str]
affected_catalogs: list[CatalogName]

@classmethod
def from_json(cls, impact: JSON):
Expand All @@ -58,8 +67,9 @@ def to_json(self) -> JSON:
affected_catalogs=self.affected_catalogs)

def validate(self):
require(all(isinstance(c, str) and c for c in self.affected_catalogs),
'Invalid catalog name/pattern')
require(all(
isinstance(c, CatalogName) and c for c in self.affected_catalogs),
'Invalid catalog name/pattern')
require(all({0: True, 1: c[-1] == '*'}.get(c.count('*'), False)
for c in self.affected_catalogs),
'Invalid catalog pattern')
Expand All @@ -75,7 +85,7 @@ class MaintenanceEvent:
actual_end: datetime | None

@classmethod
def from_json(cls, event: JSON):
def from_json(cls, event: JSON) -> Self:
return cls(planned_start=cls._parse_datetime(event['planned_start']),
planned_duration=timedelta(seconds=event['planned_duration']),
description=event['description'],
Expand Down Expand Up @@ -154,9 +164,9 @@ def validate(self):
def pending_events(self) -> list[MaintenanceEvent]:
"""
Returns a list of pending events in this schedule. The elements in the
returned list can be modified until another method is invoked on this schedule. The
modifications will be reflected in ``self.events`` but the caller is
responsible for ensuring they don't invalidate this schedule.
returned list can be modified until another method is invoked on this
schedule. The modifications will be reflected in ``self.events`` but the
caller is responsible for ensuring they don't invalidate this schedule.
"""
events = enumerate(self.events)
for i, e in events:
Expand Down Expand Up @@ -223,3 +233,55 @@ def _heal(self,
next_event.planned_start = event.end
event = next_event
self.validate()


class MaintenanceService:

@property
def bucket(self):
return aws.shared_bucket

@property
def object_key(self):
return f'azul/{config.deployment_stage}/azul.json'

@cached_property
def storage_service(self):
return StorageService(self.bucket)

def list_upcoming_events(self, all_events: bool = False) -> JSON:
"""
Display's the schedule, of active and pending events
"""
schedule = MaintenanceSchedule.from_json(self._get_schedule)
schedule.validate()

if not all_events:
active = schedule.active_event()
schedule = MaintenanceSchedule([active])
schedule.events += schedule.pending_events()
# Business logic and validation
# Start of an event
# if: actual_start is set [This is the start]
# else: planned_start
# Ending of event
# if: actual_end is set [This is the end]
return schedule.to_json()

@property
def _get_schedule(self) -> JSON:
try:
schedule = json.loads(self.storage_service.get(self.object_key))
except self.storage_service.client.exceptions.NoSuchKey:
schedule = self._create_empty_schedule
return schedule['maintenance']['schedule']

@property
def _create_empty_schedule(self):
schedule = {
'maintenance': {
'schedule': {}
}
}
self.storage_service.put(self.object_key, json.dumps(schedule).encode())
return schedule

0 comments on commit 393914b

Please sign in to comment.