• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SwissDataScienceCenter / renku-data-services / 19145569596

06 Nov 2025 06:17PM UTC coverage: 86.819% (-0.03%) from 86.85%
19145569596

Pull #1101

github

web-flow
Merge 252d68849 into fabb4724c
Pull Request #1101: feat: indicate supported platforms when checking a session image

241 of 314 new or added lines in 12 files covered. (76.75%)

8 existing lines in 4 files now uncovered.

23054 of 26554 relevant lines covered (86.82%)

1.52 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

41.38
/components/renku_data_services/notebooks/image_check.py
1
"""Functions for checking access to images.
2

3
Access to docker images can fall into these cases:
4

5
1. The image is public and exists. It can be accessed anonymously
6
2. The image cannot be found. It may be absent or it requires credentials to access it
7

8
For the latter case, try to find out as much as possible:
9
- Look for credentials in the connected services
10
- If there are no connections defined for that user and registry, image is not accessible
11
- Try access it with the credentials, if it still fails the token could be invalid.
12
- Try to obtain the connected account that checks the token validity
13
"""
14

15
from __future__ import annotations
2✔
16

17
from dataclasses import dataclass, field
2✔
18
from typing import final
2✔
19

20
import httpx
2✔
21
from authlib.integrations.httpx_client import OAuthError
2✔
22

23
from renku_data_services.app_config import logging
2✔
24
from renku_data_services.base_models.core import APIUser
2✔
25
from renku_data_services.connected_services.db import ConnectedServicesRepository
2✔
26
from renku_data_services.connected_services.models import ImageProvider, OAuth2Client, OAuth2Connection
2✔
27
from renku_data_services.errors import errors
2✔
28
from renku_data_services.notebooks.api.classes.image import Image, ImageRepoDockerAPI
2✔
29
from renku_data_services.notebooks.config import NotebooksConfig
2✔
30
from renku_data_services.notebooks.oci.models import Platform
2✔
31
from renku_data_services.notebooks.oci.utils import get_image_platforms
2✔
32

33
logger = logging.getLogger(__name__)
2✔
34

35

36
@final
2✔
37
@dataclass(frozen=True)
2✔
38
class CheckResult:
2✔
39
    """Result of checking access to an image."""
40

41
    accessible: bool
2✔
42
    platforms: list[Platform] | None
2✔
43
    response_code: int
2✔
44
    image_provider: ImageProvider | None = None
2✔
45
    token: str | None = field(default=None, repr=False)
2✔
46
    error: errors.UnauthorizedError | None = None
2✔
47

48
    def __str__(self) -> str:
2✔
49
        token = "***" if self.token else "None"
×
50
        error = "unauthorized" if self.error else "None"
×
51
        return (
×
52
            "CheckResult("
53
            f"accessible={self.accessible}/{self.response_code}, "
54
            f"provider={self.image_provider}, token={token}, error={error})"
55
        )
56

57
    @property
2✔
58
    def connection(self) -> OAuth2Connection | None:
2✔
59
        """Return the connection if present."""
60
        if self.image_provider is None:
×
61
            return None
×
62
        if self.image_provider.connected_user is None:
×
63
            return None
×
64
        return self.image_provider.connected_user.connection
×
65

66
    @property
2✔
67
    def client(self) -> OAuth2Client | None:
2✔
68
        """Return the OAuth2 client if present."""
69
        if self.image_provider is None:
×
70
            return None
×
71
        return self.image_provider.provider
×
72

73
    @property
2✔
74
    def user(self) -> APIUser | None:
2✔
75
        """Return the connected user if applicable."""
76
        if self.image_provider is None:
×
77
            return None
×
78
        if self.image_provider.connected_user is None:
×
79
            return None
×
80
        return self.image_provider.connected_user.user
×
81

82

83
class ImageCheckRepository:
2✔
84
    """Repository for checking session images with rich responses."""
85

86
    def __init__(self, nb_config: NotebooksConfig, connected_services_repo: ConnectedServicesRepository) -> None:
2✔
87
        self.nb_config = nb_config
2✔
88
        self.connected_services_repo = connected_services_repo
2✔
89

90
    async def check_image(self, user: APIUser, gitlab_user: APIUser | None, image: Image) -> CheckResult:
2✔
91
        """Check access to the given image and provide image and access details."""
NEW
92
        reg_api: ImageRepoDockerAPI = image.repo_api()  # public images
×
NEW
93
        unauth_error: errors.UnauthorizedError | None = None
×
NEW
94
        image_provider = await self.connected_services_repo.get_provider_for_image(user, image)
×
NEW
95
        connected_user = image_provider.connected_user if image_provider is not None else None
×
NEW
96
        connection = connected_user.connection if connected_user is not None else None
×
NEW
97
        if image_provider is not None:
×
NEW
98
            try:
×
NEW
99
                reg_api = await self.connected_services_repo.get_image_repo_client(image_provider)
×
NEW
100
            except errors.UnauthorizedError as e:
×
NEW
101
                logger.info(f"Error getting image repo client for image {image}: {e}")
×
NEW
102
                unauth_error = e
×
NEW
103
            except OAuthError as e:
×
NEW
104
                logger.info(f"Error getting image repo client for image {image}: {e}")
×
NEW
105
                unauth_error = errors.UnauthorizedError(
×
106
                    message=f"OAuth error when getting repo client for image: {image}"
107
                )
NEW
108
                unauth_error.__cause__ = e
×
NEW
109
        elif gitlab_user and gitlab_user.access_token and image.hostname == self.nb_config.git.registry:
×
NEW
110
            logger.debug(f"Using internal gitlab at {self.nb_config.git.registry}")
×
NEW
111
            reg_api = reg_api.with_oauth2_token(gitlab_user.access_token)
×
112

UNCOV
113
        try:
×
NEW
114
            status_code, response = await reg_api.image_check(image, include_manifest=True)
×
NEW
115
        except httpx.HTTPError as e:
×
NEW
116
            logger.info(f"Error connecting {reg_api.scheme}://{reg_api.hostname}: {e}")
×
NEW
117
            status_code = 0
×
NEW
118
            response = None
×
119

NEW
120
        if status_code != 200 and connection is not None:
×
NEW
121
            try:
×
NEW
122
                await self.connected_services_repo.get_oauth2_connected_account(connection.id, user)
×
NEW
123
            except errors.UnauthorizedError as e:
×
NEW
124
                logger.info(f"Error getting connected account: {e}")
×
NEW
125
                unauth_error = e
×
126

NEW
127
        platforms = None
×
NEW
128
        if status_code == 200 and response is not None:
×
NEW
129
            platforms = await get_image_platforms(manifest_response=response, image=image, reg_api=reg_api)
×
NEW
130
        logger.info(f"Platforms: {platforms}")
×
131

NEW
132
        return CheckResult(
×
133
            accessible=status_code == 200,
134
            platforms=platforms,
135
            response_code=status_code,
136
            image_provider=image_provider,
137
            token=reg_api.oauth2_token,
138
            error=unauth_error,
139
        )
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc