-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #11 from lsst-sqre/tickets/DM-46954
Add 'final' and 'strip' to hooks, rework 'ensure_lab' and github on demand
- Loading branch information
Showing
13 changed files
with
240 additions
and
100 deletions.
There are no files selected for viewing
This file contains 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 |
---|---|---|
@@ -1 +1 @@ | ||
{"openapi": "3.1.0", "info": {"title": "Ghostwriter", "description": "URL shortener/personalizer for Phalanx\n\n[Return to Ghostwriter documentation](.)", "version": "0.1.2.dev6+ge335638.d20241017"}, "paths": {"/ghostwriter/": {"get": {"summary": "Application metadata", "description": "Document the top-level API here. By default it only returns metadata about the application.", "operationId": "get_index_ghostwriter__get", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Index"}}}}}}}, "/ghostwriter/rewrite/{full_path}": {"get": {"summary": "Rewrite", "operationId": "rewrite_ghostwriter_rewrite__full_path__get", "parameters": [{"name": "full_path", "in": "path", "required": true, "schema": {"type": "string", "title": "The URL path to rewrite"}}], "responses": {"307": {"description": "Successful Response"}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}}, "components": {"schemas": {"HTTPValidationError": {"properties": {"detail": {"items": {"$ref": "#/components/schemas/ValidationError"}, "type": "array", "title": "Detail"}}, "type": "object", "title": "HTTPValidationError"}, "Index": {"properties": {"metadata": {"$ref": "#/components/schemas/Metadata", "title": "Package metadata"}}, "type": "object", "required": ["metadata"], "title": "Index", "description": "Metadata returned by the external root URL of the application.\n\nNotes\n-----\nAs written, this is not very useful. Add additional metadata that will be\nhelpful for a user exploring the application, or replace this model with\nsome other model that makes more sense to return from the application API\nroot."}, "Metadata": {"properties": {"name": {"type": "string", "title": "Application name", "examples": ["myapp"]}, "version": {"type": "string", "title": "Version", "examples": ["1.0.0"]}, "description": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Description", "examples": ["Some package description"]}, "repository_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Repository URL", "examples": ["https://example.com/"]}, "documentation_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Documentation URL", "examples": ["https://example.com/"]}}, "type": "object", "required": ["name", "version"], "title": "Metadata", "description": "Metadata about a package."}, "ValidationError": {"properties": {"loc": {"items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, "type": "array", "title": "Location"}, "msg": {"type": "string", "title": "Message"}, "type": {"type": "string", "title": "Error Type"}}, "type": "object", "required": ["loc", "msg", "type"], "title": "ValidationError"}}}} | ||
{"openapi": "3.1.0", "info": {"title": "Ghostwriter", "description": "URL shortener/personalizer for Phalanx\n\n[Return to Ghostwriter documentation](.)", "version": "0.1.2.dev6+ge335638.d20241015"}, "paths": {"/ghostwriter/": {"get": {"summary": "Application metadata", "description": "Document the top-level API here. By default it only returns metadata about the application.", "operationId": "get_index_ghostwriter__get", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Index"}}}}}}}, "/ghostwriter/rewrite/{full_path}": {"get": {"summary": "Rewrite", "operationId": "rewrite_ghostwriter_rewrite__full_path__get", "parameters": [{"name": "full_path", "in": "path", "required": true, "schema": {"type": "string", "title": "The URL path to rewrite"}}], "responses": {"307": {"description": "Successful Response"}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}}, "components": {"schemas": {"HTTPValidationError": {"properties": {"detail": {"items": {"$ref": "#/components/schemas/ValidationError"}, "type": "array", "title": "Detail"}}, "type": "object", "title": "HTTPValidationError"}, "Index": {"properties": {"metadata": {"$ref": "#/components/schemas/Metadata", "title": "Package metadata"}}, "type": "object", "required": ["metadata"], "title": "Index", "description": "Metadata returned by the external root URL of the application.\n\nNotes\n-----\nAs written, this is not very useful. Add additional metadata that will be\nhelpful for a user exploring the application, or replace this model with\nsome other model that makes more sense to return from the application API\nroot."}, "Metadata": {"properties": {"name": {"type": "string", "title": "Application name", "examples": ["myapp"]}, "version": {"type": "string", "title": "Version", "examples": ["1.0.0"]}, "description": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Description", "examples": ["Some package description"]}, "repository_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Repository URL", "examples": ["https://example.com/"]}, "documentation_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Documentation URL", "examples": ["https://example.com/"]}}, "type": "object", "required": ["name", "version"], "title": "Metadata", "description": "Metadata about a package."}, "ValidationError": {"properties": {"loc": {"items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, "type": "array", "title": "Location"}, "msg": {"type": "string", "title": "Message"}, "type": {"type": "string", "title": "Error Type"}}, "type": "object", "required": ["loc", "msg", "type"], "title": "ValidationError"}}}} |
This file contains 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 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 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 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,5 @@ | ||
"""Logger for hooks.""" | ||
|
||
import structlog | ||
|
||
LOGGER = structlog.getLogger("ghostwriter") |
This file contains 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,65 @@ | ||
"""Ensure that the user has a running lab. If they don't, they will be | ||
given `recommended` in `medium` size. | ||
""" | ||
|
||
import asyncio | ||
|
||
from rubin.nublado.client import NubladoClient | ||
from rubin.nublado.client.models import ( | ||
NubladoImage, | ||
NubladoImageByClass, | ||
NubladoImageClass, | ||
NubladoImageSize, | ||
) | ||
|
||
from ..models.substitution import Parameters | ||
from ._logger import LOGGER | ||
|
||
LAB_SPAWN_TIMEOUT = 90 | ||
|
||
|
||
async def ensure_autostart_lab(params: Parameters) -> None: | ||
"""Start a Lab if one is not present.""" | ||
LOGGER.debug(f"Checking for running Lab for {params}") | ||
client = params.client | ||
LOGGER.debug("Logging in to Hub") | ||
await client.auth_to_hub() | ||
stopped = await client.is_lab_stopped() | ||
if stopped: | ||
LOGGER.debug(f"Starting new 'recommended' Lab for {params.user}") | ||
await _spawn_lab(client) | ||
else: | ||
LOGGER.debug(f"{params.user} already has a running Lab") | ||
LOGGER.debug("Lab spawned; proceeding with redirection") | ||
|
||
|
||
async def _spawn_lab(client: NubladoClient) -> None: | ||
image = _choose_image() | ||
await client.spawn_lab(image) | ||
await _follow_progress(client) | ||
|
||
|
||
def _choose_image() -> NubladoImage: | ||
"""Because the primary use case of this redirection service is | ||
to launch tutorial notebooks, our "start a new lab" parameters | ||
are going to be the currently recommended lab, and Medium size, | ||
because that's how the tutorial notebooks are generally set up | ||
to run. Maybe we will do something more sophisticated later. | ||
""" | ||
return NubladoImageByClass( | ||
image_class=NubladoImageClass.RECOMMENDED, size=NubladoImageSize.Medium | ||
) | ||
|
||
|
||
async def _follow_progress(client: NubladoClient) -> None: | ||
LOGGER.debug("Waiting for lab to spawn") | ||
progress = client.watch_spawn_progress() | ||
try: | ||
async with asyncio.timeout(LAB_SPAWN_TIMEOUT): | ||
async for message in progress: | ||
LOGGER.debug(f"Lab spawn message: {message.message}") | ||
if message.ready: | ||
break | ||
except TimeoutError: | ||
LOGGER.exception(f"Lab did not spawn in {LAB_SPAWN_TIMEOUT} seconds") | ||
raise |
This file contains 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 |
---|---|---|
@@ -1,64 +1,35 @@ | ||
"""Ensure that the user has a running lab.""" | ||
|
||
import asyncio | ||
|
||
import structlog | ||
from rubin.nublado.client import NubladoClient | ||
from rubin.nublado.client.models import ( | ||
NubladoImage, | ||
NubladoImageByClass, | ||
NubladoImageClass, | ||
NubladoImageSize, | ||
) | ||
"""Ensure that the user has a running lab by dropping them in the spawner form | ||
if they don't, and then slingshotting them through the lab ghostwriter endpoint | ||
to restart the redirect process. Requires d_2024_10_19 (corresponds to | ||
w_2024_10_43) or later. | ||
""" | ||
|
||
from ..models.substitution import Parameters | ||
|
||
LAB_SPAWN_TIMEOUT = 90 | ||
LOGGER = structlog.get_logger("ghostwriter") | ||
from ._logger import LOGGER | ||
|
||
|
||
async def ensure_running_lab(params: Parameters) -> None: | ||
async def ensure_running_lab(params: Parameters) -> Parameters | None: | ||
"""Start a Lab if one is not present.""" | ||
LOGGER.debug(f"Checking for running Lab for {params}") | ||
client = params.client | ||
LOGGER.debug("Logging in to Hub") | ||
await client.auth_to_hub() | ||
stopped = await client.is_lab_stopped() | ||
if stopped: | ||
LOGGER.debug(f"Starting new 'recommended' Lab for {params.user}") | ||
await _spawn_lab(client) | ||
else: | ||
if not stopped: | ||
LOGGER.debug(f"{params.user} already has a running Lab") | ||
LOGGER.debug("Lab spawned; proceeding with redirection") | ||
|
||
|
||
async def _spawn_lab(client: NubladoClient) -> None: | ||
image = _choose_image() | ||
await client.spawn_lab(image) | ||
await _follow_progress(client) | ||
|
||
|
||
def _choose_image() -> NubladoImage: | ||
"""Because the primary use case of this redirection service is | ||
to launch tutorial notebooks, our "start a new lab" parameters | ||
are going to be the currently recommended lab, and Medium size, | ||
because that's how the tutorial notebooks are generally set up | ||
to run. Maybe we will do something more sophisticated later. | ||
""" | ||
return NubladoImageByClass( | ||
image_class=NubladoImageClass.RECOMMENDED, size=NubladoImageSize.Medium | ||
return None | ||
LOGGER.debug(f"Sending {params.user} to spawner") | ||
LOGGER.debug(f"Input parameters {params}") | ||
new_p = Parameters( | ||
user=params.user, | ||
base_url=params.base_url, | ||
path=params.path, | ||
token=params.token, | ||
client=params.client, | ||
target="${base_url}/nb/user/${user}/rubin/ghostwriter/${path}", | ||
unique_id=params.unique_id, | ||
strip=False, | ||
final=True, | ||
) | ||
|
||
|
||
async def _follow_progress(client: NubladoClient) -> None: | ||
LOGGER.debug("Waiting for lab to spawn") | ||
progress = client.watch_spawn_progress() | ||
try: | ||
async with asyncio.timeout(LAB_SPAWN_TIMEOUT): | ||
async for message in progress: | ||
LOGGER.debug(f"Lab spawn message: {message.message}") | ||
if message.ready: | ||
break | ||
except TimeoutError: | ||
LOGGER.exception(f"Lab did not spawn in {LAB_SPAWN_TIMEOUT} seconds") | ||
raise | ||
LOGGER.debug(f"Output parameters {new_p}") | ||
return new_p |
This file contains 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 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 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 |
---|---|---|
@@ -1,11 +1,9 @@ | ||
"""Does nothing, but if it loads, the hook machinery is working.""" | ||
|
||
import structlog | ||
|
||
from ..models.substitution import Parameters | ||
from ._logger import LOGGER | ||
|
||
|
||
async def vacuous_hook(params: Parameters) -> None: | ||
"""Load and execute a no-op.""" | ||
logger = structlog.get_logger("ghostwriter") | ||
logger.debug(f"Vacuous hook called with {params}") | ||
LOGGER.debug(f"Vacuous hook called with {params}") |
Oops, something went wrong.