Skip to content

Commit

Permalink
refactor(updater): make room for multiple update policies
Browse files Browse the repository at this point in the history
  • Loading branch information
fyhertz committed Jan 10, 2022
1 parent 320df8b commit 72fab01
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 57 deletions.
4 changes: 4 additions & 0 deletions src/ops2deb/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ class Ops2debFetcherError(Ops2debError):
pass


class Ops2debUpdaterWarning(Ops2debError):
pass


class Ops2debUpdaterError(Ops2debError):
pass

Expand Down
146 changes: 89 additions & 57 deletions src/ops2deb/updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from .exceptions import Ops2debUpdaterError
from .fetcher import Fetcher, FetchResult, FetchResultOrError
from .parser import Blueprint, RemoteFile, extend, load, validate
from .utils import log_and_raise, separate_successes_from_errors
from .utils import separate_successes_from_errors


# fixme: move this somewhere else, this code is also duplicated in formatter.py
Expand All @@ -28,79 +28,111 @@ class LatestRelease:
blueprint: Blueprint
version: str


async def _bump_and_poll(
client: httpx.AsyncClient,
blueprint: Blueprint,
version: Version,
bump_patch: bool = False,
) -> Version:
new_version = version
while True:
version = version.bump_patch() if bump_patch else version.bump_minor()
if not (remote_file := blueprint.render_fetch(version=str(version))):
break
url = remote_file.url
logger.debug(f"Trying {url}")
@property
def is_new(self) -> bool:
return self.blueprint.version != self.version


class BaseUpdater:
def __init__(self, client: httpx.AsyncClient):
self.client = client

@classmethod
def is_blueprint_supported(cls, blueprint: Blueprint) -> bool:
raise NotImplementedError

async def __call__(self, blueprint: Blueprint) -> str:
raise NotImplementedError


class GenericUpdater(BaseUpdater):
async def _bump_and_poll(
self,
blueprint: Blueprint,
version: Version,
bump_patch: bool = False,
) -> Version:
new_version = version
while True:
version = version.bump_patch() if bump_patch else version.bump_minor()
if not (remote_file := blueprint.render_fetch(version=str(version))):
break
url = remote_file.url
logger.debug(f"Trying {url}")
try:
response = await self.client.head(url)
except httpx.HTTPError as e:
raise Ops2debUpdaterError(f"Failed HEAD request to {url}. {str(e)}")
status = response.status_code
if status >= 500:
raise Ops2debUpdaterError(f"Server error when requesting {url}")
if status >= 400:
break
else:
new_version = version
return new_version

@classmethod
def is_blueprint_supported(cls, blueprint: Blueprint) -> bool:
if not Version.isvalid(blueprint.version):
logger.warning(f"{blueprint.name} is not using semantic versioning")
return False
return True

async def __call__(self, blueprint: Blueprint) -> str:
version = Version.parse(blueprint.version)
version = await self._bump_and_poll(blueprint, version, False)
version = await self._bump_and_poll(blueprint, version, True)
return str(version)


async def _find_latest_version(client: httpx.AsyncClient, blueprint: Blueprint) -> str:
updaters = [GenericUpdater(client)]
updaters = [u for u in updaters if u.is_blueprint_supported(blueprint)]
for updater in updaters:
try:
response = await client.head(url)
except httpx.HTTPError as e:
log_and_raise(Ops2debUpdaterError(f"Failed HEAD request to {url}. {str(e)}"))
status = response.status_code
if status >= 500:
log_and_raise(Ops2debUpdaterError(f"Server error when requesting {url}"))
if status >= 400:
break
else:
new_version = version
return new_version
return await updater(blueprint)
except Ops2debUpdaterError as e:
logger.debug(str(e))
continue
error = f"Failed to update {blueprint.name}, enable debug logs for more information"
logger.error(error)
raise Ops2debUpdaterError(error)


async def _find_latest_release(
blueprint: Blueprint,
) -> Optional[LatestRelease]:
if blueprint.fetch is None:
return None

if not Version.isvalid(blueprint.version):
logger.warning(f"{blueprint.name} is not using semantic versioning")
return None

old_version = version = Version.parse(blueprint.version)
async with client_factory() as client:
version = await _bump_and_poll(client, blueprint, version, False)
version = await _bump_and_poll(client, blueprint, version, True)

if version != old_version:
logger.info(f"{blueprint.name} can be bumped from {old_version} to {version}")

return LatestRelease(
blueprint=blueprint,
version=str(version),
client: httpx.AsyncClient, blueprint: Blueprint
) -> LatestRelease:
version = await _find_latest_version(client, blueprint)
if blueprint.version != version:
logger.info(
f"{blueprint.name} can be bumped from {blueprint.version} to {version}"
)

return None
return LatestRelease(blueprint, version)


def _find_latest_releases(
blueprints: List[Blueprint],
) -> List[Optional[Union[LatestRelease, Exception]]]:
) -> List[Union[LatestRelease, Exception]]:
async def run_tasks() -> Any:
return await asyncio.gather(
*[_find_latest_release(b) for b in blueprints],
return_exceptions=True,
)
async with client_factory() as client:
tasks = [
_find_latest_release(client, blueprint)
for blueprint in blueprints
if blueprint.fetch is not None
]
return await asyncio.gather(*tasks, return_exceptions=True)

return asyncio.run(run_tasks())


def _fetch_latest_files(
latest_releases: List[Optional[LatestRelease]],
latest_releases: List[LatestRelease],
) -> Dict[str, FetchResultOrError]:
blueprints = [
release.blueprint.copy(update={"version": release.version})
for release in latest_releases
if release is not None
if release.blueprint.version != release.version
]
remote_files = [cast(RemoteFile, b.render_fetch()) for b in extend(blueprints)]
fetcher = Fetcher(remote_files)
Expand Down Expand Up @@ -157,7 +189,7 @@ def update(

if dry_run is False and latest_releases:
for raw_blueprint, release in zip(raw_blueprints, updater_results):
if isinstance(release, LatestRelease):
if isinstance(release, LatestRelease) and release.is_new:
_update_raw_blueprint(raw_blueprint, release, fetch_results)
with configuration_path.open("w") as output:
yaml.dump(configuration_dict, output)
Expand All @@ -167,7 +199,7 @@ def update(
lines = [
f"Updated {r.blueprint.name} from {r.blueprint.version} to {r.version}"
for r in latest_releases
if isinstance(r, LatestRelease)
if r.is_new
]
output_path.write_text("\n".join(lines) + "\n")

Expand Down

0 comments on commit 72fab01

Please sign in to comment.