diff --git a/hub_adapter/auth.py b/hub_adapter/auth.py index 71f844b..133cf0d 100644 --- a/hub_adapter/auth.py +++ b/hub_adapter/auth.py @@ -101,7 +101,13 @@ async def verify_idp_token(token: str = Security(idp_oauth2_scheme)) -> dict: async def get_hub_token() -> dict: """Automated method for getting a token from the central Hub service.""" hub_user, hub_pwd = hub_adapter_settings.HUB_USERNAME, hub_adapter_settings.HUB_PASSWORD - payload = {"username": hub_user, "password": hub_pwd} + payload = {"username": hub_user, "password": hub_pwd} # For testing + + # TODO move to robot + # robot_user, robot_secret = hub_adapter_settings.HUB_ROBOT_USER, hub_adapter_settings.HUB_ROBOT_SECRET + # {"grant_type": 'robot_credentials', "id": '|', "secret": ''} + # payload = {"grant_type": 'robot_credentials', "id": robot_user, "secret": robot_secret} + token_route = hub_adapter_settings.HUB_AUTH_SERVICE_URL.rstrip("/") + "/token" resp = httpx.post(token_route, data=payload) diff --git a/hub_adapter/conf.py b/hub_adapter/conf.py index 046ed4d..7e0495f 100644 --- a/hub_adapter/conf.py +++ b/hub_adapter/conf.py @@ -29,10 +29,12 @@ class Settings(BaseModel): API_CLIENT_SECRET: str = os.getenv("API_CLIENT_SECRET") # Not used currently # Hub - HUB_AUTH_SERVICE_URL: str = os.getenv("HUB_AUTH_SERVICE_URL", "https://auth.privateaim.net") - HUB_SERVICE_URL: str = os.getenv("HUB_SERVICE_URL", "https://api.privateaim.net") + HUB_AUTH_SERVICE_URL: str = os.getenv("HUB_AUTH_SERVICE_URL", "https://privateaim.net/auth") + HUB_SERVICE_URL: str = os.getenv("HUB_SERVICE_URL", "https://privateaim.net/core") HUB_USERNAME: str = os.getenv("HUB_USERNAME") HUB_PASSWORD: str = os.getenv("HUB_PASSWORD") + HUB_ROBOT_USER: str = os.getenv("HUB_ROBOT_USER") + HUB_ROBOT_SECRET: str = os.getenv("HUB_ROBOT_SECRET") hub_adapter_settings = Settings() diff --git a/hub_adapter/models/hub.py b/hub_adapter/models/hub.py index fb7b497..5c4b6e9 100644 --- a/hub_adapter/models/hub.py +++ b/hub_adapter/models/hub.py @@ -165,3 +165,18 @@ class ListAnalysisNodes(BaseModel): class ListContainers(BaseModel): containers: list[ContainerData] + + +class RegistryProject(BaseHubResponse): + name: str | None = None + type: str + public: bool + external_name: str | None = None + external_id: str | None = None + webhook_name: str | None = None + webhook_exists: bool | None = None + account_name: str | None = None + account_secret: str | None = None + registry_id: uuid.UUID | None = None + registry: Registry | None = None + realm_id: uuid.UUID | None = None diff --git a/hub_adapter/routers/hub.py b/hub_adapter/routers/hub.py index 58ab5e8..f9547aa 100644 --- a/hub_adapter/routers/hub.py +++ b/hub_adapter/routers/hub.py @@ -2,68 +2,68 @@ import uuid from typing import Annotated -from fastapi import APIRouter, Query, Path, Depends, Security +from fastapi import APIRouter, Query, Path, Depends from starlette import status from starlette.requests import Request -from starlette.responses import Response +from starlette.responses import Response, RedirectResponse -from hub_adapter.auth import add_hub_jwt, verify_idp_token, idp_oauth2_scheme_pass, httpbearer +from hub_adapter.auth import add_hub_jwt from hub_adapter.conf import hub_adapter_settings from hub_adapter.core import route from hub_adapter.models.hub import Project, AllProjects, ApprovalStatus, AnalysisOrProjectNode, \ ListAnalysisOrProjectNodes, \ - AnalysisNode, ListAnalysisNodes -from hub_adapter.models.podorc import ImageDataResponse + AnalysisNode, ListAnalysisNodes, RegistryProject hub_router = APIRouter( - dependencies=[Security(verify_idp_token), Depends(add_hub_jwt), Security(idp_oauth2_scheme_pass), - Security(httpbearer)], + # dependencies=[Security(verify_idp_token), Depends(add_hub_jwt), Security(idp_oauth2_scheme_pass), + # Security(httpbearer)], + dependencies=[Depends(add_hub_jwt)], tags=["Hub"], responses={404: {"description": "Not found"}}, ) -@hub_router.get("/hub/images", response_model=ImageDataResponse) -async def get_images(): - """Return list of images for the frontend.""" - # TODO: replace with data from https://api.privateaim.net/master-images - - dummy_data = { - "pullImages": [ - { - "id": "59081687-3dfe-46cf-afb5-07c562a002af", - "train_class_id": "choochoo", - "repo_tag": "0.5.23-pull", - "job_id": "49e79b47-686b-4fb8-9259-fd0035b0b7f6", - "status": "pulled" - } - ], - "pushImages": [ - { - "id": "4a941577-46ce-4220-8ca0-181cf45abe29", - "train_class_id": "choochoo", - "repo_tag": "latest", - "job_id": "5efabb71-ba5d-4d00-9ed4-f27eb6a52e8f", - "status": "waiting_to_push" - } - ], - } - return dummy_data - - -@hub_router.get("/hub/vault/status") -async def get_vault_status(): - """Spoof vault status.""" - dummy_data = { - "initialized": True, - "sealed": False, - "authenticated": True, - "config": { - "stationID": "4c0e4a1a-795b", - "stationName": "Test FLAME Node Central", - } - } - return dummy_data +# @hub_router.get("/hub/images", response_model=ImageDataResponse) +# async def get_images(): +# """Return list of images for the frontend.""" +# # TODO: replace with data from https://api.privateaim.net/master-images +# +# dummy_data = { +# "pullImages": [ +# { +# "id": "59081687-3dfe-46cf-afb5-07c562a002af", +# "train_class_id": "choochoo", +# "repo_tag": "0.5.23-pull", +# "job_id": "49e79b47-686b-4fb8-9259-fd0035b0b7f6", +# "status": "pulled" +# } +# ], +# "pushImages": [ +# { +# "id": "4a941577-46ce-4220-8ca0-181cf45abe29", +# "train_class_id": "choochoo", +# "repo_tag": "latest", +# "job_id": "5efabb71-ba5d-4d00-9ed4-f27eb6a52e8f", +# "status": "waiting_to_push" +# } +# ], +# } +# return dummy_data +# +# +# @hub_router.get("/hub/vault/status") +# async def get_vault_status(): +# """Spoof vault status.""" +# dummy_data = { +# "initialized": True, +# "sealed": False, +# "authenticated": True, +# "config": { +# "stationID": "4c0e4a1a-795b", +# "stationName": "Test FLAME Node Central", +# } +# } +# return dummy_data @route( @@ -248,11 +248,19 @@ async def list_analyses_of_nodes( status_code=status.HTTP_200_OK, service_url=hub_adapter_settings.HUB_SERVICE_URL, response_model=AnalysisNode, + query_params=["include"], ) async def list_specific_analysis( - analysis_id: Annotated[uuid.UUID, Path(description="Analysis UUID.")], request: Request, response: Response, + analysis_id: Annotated[uuid.UUID, Path(description="Analysis UUID.")], + include: Annotated[ + str | None, + Query( + description="Whether to include additional data for the given parameter. Can only be 'node'/'analysis'", + pattern="^((^|[,])(analysis|node))+$", # Must be "node" or "analysis" or null, + ), + ] = "analysis", ): """List project for a given UUID.""" pass @@ -276,3 +284,74 @@ async def accept_reject_analysis_node( ): """Set the approval status of a analysis.""" pass + + +@route( + request_method=hub_router.get, + path="/registry-projects/{registry_project_id}", + status_code=status.HTTP_200_OK, + service_url=hub_adapter_settings.HUB_SERVICE_URL, + response_model=RegistryProject, + query_params=["include"], +) +async def get_registry_metadata_for_project( + request: Request, + response: Response, + include: Annotated[ + str | None, + Query( + description="Whether to include additional registry data. Can only be 'registry'", + pattern="^registry$", # Must be "registry" or null, + ), + ] = "registry", +): + """List registry data for a project.""" + pass + + +def get_analysis_metadata_for_url(analysis_id: uuid.UUID = Path(description="UUID of analysis.")): + """Get analysis metadata for a given UUID to be used in creating analysis image URL.""" + analysis_url = hub_router.url_path_for("list_specific_analysis", analysis_id=analysis_id) + analysis_resp = RedirectResponse(analysis_url + "?include=analysis,node") + return analysis_resp + + +def get_registry_metadata_for_url(analysis_metadata: dict = Depends(get_analysis_metadata_for_url)): + """Get registry metadata for a given UUID to be used in creating analysis image URL.""" + registry_project_id = analysis_metadata["node"]["registry_project_id"] + registry_url = hub_router.url_path_for( + "get_registry_metadata_for_project", + registry_project_id=registry_project_id + ) + registry_resp = RedirectResponse( + registry_url + "?include=registry&fields=+account_id,+account_name,+account_secret" + ) + return registry_resp + + +@hub_router.get("/analysis/image/{analysis_id}", response_class=RedirectResponse) +async def get_analysis_image_url( + # analysis_id: Annotated[uuid.UUID, Path(description="UUID of analysis.")], + registry_metadata: dict = Depends(get_registry_metadata_for_url), + +) -> dict: + """Build an analysis image URL using its metadata from the Hub.""" + aid = 2 + # analysis_url = hub_router.url_path_for("list_specific_analysis", analysis_id=analysis_id) + # analysis_resp = RedirectResponse(analysis_url + "?include=analysis,node") + + # analysis_resp = get_analysis_metadata_for_url(analysis_id) + + # registry_project_id = analysis_metadata["node"]["registry_project_id"] + + # registry_url = hub_router.url_path_for( + # "get_registry_metadata_for_project", + # registry_project_id=registry_project_id + # ) + # registry_resp = RedirectResponse( + # registry_url + "?include=registry&fields=+account_id,+account_name,+account_secret" + # ) + registry_external_name = registry_metadata["external_name"] + host = registry_metadata["registry"]["host"] + + return {"image_url": f"{host}/{registry_external_name}/{aid}"}