diff --git a/core/services/versionchooser/test_versionchooser.py b/core/services/versionchooser/test_versionchooser.py index e2180c9146..a40828fd6d 100644 --- a/core/services/versionchooser/test_versionchooser.py +++ b/core/services/versionchooser/test_versionchooser.py @@ -6,6 +6,7 @@ import pytest from utils.chooser import VersionChooser +from utils.dockerhub import TagFetcher, TagMetadata # All test coroutines will be treated as marked. pytestmark = pytest.mark.asyncio @@ -236,3 +237,41 @@ async def is_valid_version(image: str) -> Tuple[bool, str]: result = await chooser.set_version("bluerobotics/blueos-core", "master") assert result.status_code == 500 assert len(json_mock.mock_calls) > 0 + + +class TestTagFetcher: + """Test class for TagFetcher functionality""" + + @pytest.mark.asyncio + async def test_fetch_real_blueos_core_tags(self) -> None: + """Integration test: Fetch real tags from bluerobotics/blueos-core repository""" + fetcher = TagFetcher() + + try: + errors, tags = await fetcher.fetch_remote_tags("bluerobotics/blueos-core", []) + + # Verify we got some tags back + assert isinstance(tags, list) + assert len(tags) > 0, "Should have found some tags for bluerobotics/blueos-core" + + # Verify tag structure + for tag in tags[:3]: # Check first 3 tags + assert isinstance(tag, TagMetadata) + assert tag.repository == "bluerobotics/blueos-core" + assert tag.image == "blueos-core" + assert tag.tag is not None + assert len(tag.tag) > 0 + assert tag.last_modified is not None + assert tag.digest is not None + + # Should find the 'master' tag + tag_names = [tag.tag for tag in tags] + assert "master" in tag_names, f"Expected to find 'master' tag in tags: {tag_names[:10]}" + + # Errors should be empty string if successful + if errors: + print(f"Non-fatal errors during fetch: {errors}") + + except Exception as e: + # If this fails due to network issues, skip the test + pytest.skip(f"Could not fetch tags from DockerHub, likely network issue: {e}") diff --git a/core/services/versionchooser/utils/chooser.py b/core/services/versionchooser/utils/chooser.py index 0c86006112..4ba0bec446 100644 --- a/core/services/versionchooser/utils/chooser.py +++ b/core/services/versionchooser/utils/chooser.py @@ -1,10 +1,11 @@ +from dataclasses import asdict import json import pathlib import sys -from dataclasses import asdict from datetime import datetime from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple, Union +import aiohttp import aiodocker import appdirs import docker @@ -12,7 +13,7 @@ from fastapi.responses import JSONResponse, StreamingResponse from loguru import logger -from utils.dockerhub import TagFetcher +from utils.dockerhub import get_current_arch, TagFetcher DOCKER_CONFIG_PATH = pathlib.Path(appdirs.user_config_dir("bootstrap"), "startup.json") @@ -339,20 +340,49 @@ async def set_local_versions(self, output: Dict[str, Optional[Union[str, List[Di } ) - async def set_remote_versions( + async def get_remote_versions_from_blueos_registry(self, index: str, repository: str) -> List[Dict[str, Any]]: + arch = get_current_arch() + try: + async with aiohttp.ClientSession() as session: + async with session.get( + f"http://logtools.cloud:8080/GetImages?repository={repository}&arch={arch}&index={index}" + ) as resp: + if resp.status != 200: + logger.warning(f"Error status {resp.status}") + data = await resp.json(content_type=None) + return data["tags"] + except Exception as error: + logger.critical(f"error fetching online tags: {error}") + return [] + + def get_selected_index(self) -> str: + return "dockerhub" + + async def set_remote_versions_from_dockerhub( self, output: Dict[str, Optional[Union[str, List[Dict[str, Any]]]]], repository: str - ) -> None: + ) -> Dict[str, Optional[Union[str, List[Dict[str, Any]]]]]: try: - assert isinstance(output["local"], list) output["error"], online_tags = await TagFetcher().fetch_remote_tags( repository, [image["tag"] for image in output["local"]] ) + online_tags = [asdict(tag) for tag in online_tags] + output["remote"] = online_tags except Exception as error: logger.critical(f"error fetching online tags: {error}") - online_tags = [] output["error"] = f"error fetching online tags: {error}" + return output + + async def set_remote_versions( + self, output: Dict[str, Optional[Union[str, List[Dict[str, Any]]]]], repository: str + ) -> None: + index = self.get_selected_index() + online_tags = await self.get_remote_versions_from_blueos_registry(index, repository) + if len(online_tags) == 0: + logger.warning(f"No tags found using BlueOS registry cache for {repository}, fetching from DockerHub!") + output = await self.set_remote_versions_from_dockerhub(output, repository) assert isinstance(output["remote"], list) - output["remote"].extend([asdict(tag) for tag in online_tags]) + output["remote"].extend(online_tags) + return output async def get_available_local_versions(self) -> JSONResponse: output: Dict[str, Optional[Union[str, List[Dict[str, Any]]]]] = {"local": [], "error": None}