-
Notifications
You must be signed in to change notification settings - Fork 25
ci: reclaim OpenStack orphans at integration test start #808
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
cbartz
wants to merge
13
commits into
main
Choose a base branch
from
ci/openstack-ci-resource-cleanup
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
69e53e2
ci: schedule OpenStack CI resource cleanup
cbartz 7fa7b94
ci: reclaim OpenStack orphans at integration test start
cbartz 0e63df0
refactor: step-down orphan cleanup helpers
cbartz 9db6424
fix: skip orphan cleanup when resource age is unknown
cbartz c92d809
fix: address Copilot round-2 orphan cleanup nits
cbartz 7e74041
fix: delete servers by id and keypairs by name=
cbartz 9859b86
fix: call delete_keypair positionally like the rest of GRO
cbartz 482c388
fix: more Copilot nits for orphan cleanup
cbartz 13bd30b
fix: skip orphan cleanup when reusing existing app
cbartz 4c71e77
refactor: share OpenStack CI naming; drop unused orphan bits
cbartz c68d8ab
docs: spell out orphan cleanup suite scopes
cbartz 47418a7
docs: keep public docstrings to intent only
cbartz d1f60c2
docs: clarify OpenStack prefix list ordering comment
cbartz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| # Copyright 2026 Canonical Ltd. | ||
| # See LICENSE file for licensing details. | ||
|
|
||
| """Delete leftover OpenStack resources from github-runner-manager integration tests. | ||
|
|
||
| This module is used by the suite under ``github-runner-manager/tests/integration/``. | ||
| That suite runs the github-runner-manager application against a real OpenStack | ||
| cloud (servers and SSH keypairs named with the suite's ``test-runner-{id}`` | ||
| prefix from ``factories.TestConfig``). | ||
|
|
||
| When a previous CI job is force-cancelled, those OpenStack resources can be | ||
| left behind. The next time this suite starts, it calls | ||
| :func:`cleanup_stale_openstack_resources` so older leftovers are removed | ||
| before new ones are created. | ||
| """ | ||
|
|
||
| import logging | ||
| from collections.abc import Callable | ||
| from datetime import datetime, timedelta, timezone | ||
|
|
||
| from openstack.connection import Connection | ||
|
|
||
| from .factories import is_manager_openstack_resource_name | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| def cleanup_stale_openstack_resources( | ||
| connection: Connection, | ||
| min_age: timedelta = timedelta(hours=6), | ||
| ) -> None: | ||
| """Delete leftover OpenStack resources from previous suite runs older than ``min_age``.""" | ||
| now = datetime.now(tz=timezone.utc) | ||
| logger.info( | ||
| "OpenStack orphan cleanup starting (min_age=%sh)", | ||
| min_age.total_seconds() / 3600.0, | ||
| ) | ||
|
|
||
| for server in connection.list_servers(bare=True) or []: | ||
| name = getattr(server, "name", None) | ||
| if not is_manager_openstack_resource_name(name): | ||
| continue | ||
| if not _is_stale( | ||
| getattr(server, "created_at", None) or getattr(server, "created", None), | ||
| min_age, | ||
| now, | ||
| ): | ||
| continue | ||
| _safe_delete( | ||
| "server", | ||
| name or server.id, | ||
| lambda s=server: connection.delete_server(s.id, wait=True), | ||
| ) | ||
|
|
||
| for keypair in connection.list_keypairs() or []: | ||
| name = getattr(keypair, "name", None) | ||
| if not is_manager_openstack_resource_name(name): | ||
| continue | ||
| if not _is_stale(getattr(keypair, "created_at", None), min_age, now): | ||
| continue | ||
| _safe_delete("keypair", name or "", lambda n=name: connection.delete_keypair(n)) | ||
|
|
||
| logger.info("OpenStack orphan cleanup finished") | ||
|
|
||
|
|
||
| def _safe_delete(label: str, name: str, delete_fn: Callable[[], object]) -> None: | ||
| try: | ||
| delete_fn() | ||
| logger.info("Orphan cleanup deleted %s %s", label, name) | ||
| except Exception as exc: # noqa: BLE001 | ||
| logger.warning( | ||
| "Orphan cleanup failed deleting %s %s: %s", label, name, exc, exc_info=True | ||
| ) | ||
|
|
||
|
|
||
| def _is_stale(created_at: object, min_age: timedelta, now: datetime) -> bool: | ||
|
cbartz marked this conversation as resolved.
|
||
| """Return True if the resource is older than ``min_age``.""" | ||
| created = _parse_created_at(created_at) | ||
| if created is None: | ||
| return False | ||
| return now - created >= min_age | ||
|
|
||
|
|
||
| def _parse_created_at(value: object) -> datetime | None: | ||
| if value is None: | ||
| return None | ||
| if isinstance(value, datetime): | ||
| return value if value.tzinfo else value.replace(tzinfo=timezone.utc) | ||
| text = str(value).strip().replace("Z", "+00:00") | ||
| if not text: | ||
| return None | ||
| try: | ||
| dt = datetime.fromisoformat(text) | ||
| except ValueError: | ||
| return None | ||
| if dt.tzinfo is None: | ||
| dt = dt.replace(tzinfo=timezone.utc) | ||
| return dt.astimezone(timezone.utc) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.