Skip to content

Commit 8e9a9ce

Browse files
authored
feat: build from private repositories (#1250)
* feat: add private image prefix to config * feat(repositories/db): add token getter This will be used to create the secret allowing Shipwright to create images out of private repositories. * feat(session/models): add authentication secret This will allow to pass authentication information to the Shipwright client for private repositories. * feat(session/k8s_client): implement support for clone secret When getting an authentication secret in the parameter, the client will now: - create a secret for the BuildRun - configure the BuildRun to use it - patch the secret to be owned by the BuildRun object The last one ensures that the secret is delete alongside the BuildRun object. Behaviour change: the BuildRun object is refreshed so that the cache will contain its UID which is mandatory for the ownership setup. * feat(session/db): implement repo visibility check and token retrieval Now retrieving the build parameter will include the token for private repositories. * feat(session/constants): update default buildstragy to v3 It's the name of the current strategy in components/renku_pack_builder. * refactor(test): allow reuse of existing cluster in tests This allows to reduce testing time with regard to cluster creation and setup. * feat(test): add optional Shipwright setup This allows to run tests that will create BuildRun objects. * feat(test): implement support for real Shipwright client This will allow to run tests as they are currently as well as with Shipwright deployed. * refactor(test_sessions): update tests for builds They now can run with and without builds activated. Note: nothing will get pushed nor will BuildRun succeed to finish. The goal is to ensure that thing are properly working from an object creation point of view. * refactor(k8s/client): cache object once created Currently the cache is populated before the object is created and the entry is deleted if creation failed. This has two drawbacks: - requires two operations to the cache that might fail - does not store the latest version of the object if refresh is true For the latter, a third call could be done to update the cache content but it's sub-optimal. An up to date cache content is required for the support of setting the owner references of the clone secret for BuildRuns that must manage private repositories. * feat(session): push images from private repo to a different registry This allows to cleanly separate were both set of images are stored. * refactor(test/session): add BuildRun validation These checks ensure that the BuildRun objects are: - Using the proper registry to push to - Get the secret for private builds * refactor(session/db): better handling of union return types * fix(test/utils): improve fake get_repository return - "/some/repo" now returns a GitUrlError - Not managed cases calls base class implementation * refactor(test/utils): only use FakeGitRepositoriesRepository when testing builds * refactor(k8s/client): change create back to almost original implementation The optimization done somehow breaks information retrieval during test with getting duplicated entries in some tests. Revert back to the original implementation but with added cache update when object refresh is requested. * fix(test): move cluster-creation option handling in main conftest * refactor(test/sessions): move BuildRun content check behind feature check * fix: add "sessions" marker to pyproject.toml This addresses a warning from pytest * feat: ensure kind configuration file is test run specific * fix(session/db): add missing parameter to make_session_environment_repo * refactor(test): rename class starting with Test They are detected by pytest as potential tests cases although they are only helpers. Renaming fixes the associated warning. * fix(builds): add missing secret annotation for Shipwright * refactor: port deprecated Config use to model_config This fixes all the pydantic warnings and makes the code base consistant. * feat: add configuration for push secret for private registry * fix(test): make DummyAuthenticator return a AuthenticatedAPIUser This matches the behaviour of real authenticator * feat(notebooks): add check based on build parameters Ensure, when a session start, that if an image is from a built environment, the image source repository is contained in the list of repositories from the project. * feat(test): fix and implement session start tests * feat(builds): add repo accessibility check when starting a session Ensure that the user has pull access from the repository when starting a session that was built. Visibility is not enough as for example in the case of GitLab, a user can have access to the code in GitLab but is not allowed to clone the repository. See https://docs.gitlab.com/user/permissions/ and more specifically the guest role. * chore(tests): fix tuple deprecation warnings from SQLAlchemy * refactor(DummyAuthenticator): return an empty APIUser in case of missing id This distinguishes between a successful and a failed authentication attempt vs an anonymous user. * chore(test): fix handling of parametrize arguments * feat(crc): make the config file pattern more flexible While containing yaml, k8s config files do not end with .yaml by default so if testing with a local cluster, the tests will fail. * fix(tests): fix authorisation headers for resource pool creation
1 parent fe542a9 commit 8e9a9ce

51 files changed

Lines changed: 951 additions & 335 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

bases/renku_data_services/data_api/app.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ def register_all_handlers(app: Sanic, dm: DependencyManager) -> Sanic:
221221
session_repo=dm.session_repo,
222222
storage_repo=dm.storage_repo,
223223
user_repo=dm.kc_user_repo,
224+
git_repositories_repo=dm.git_repositories_repo,
224225
)
225226
platform_config = PlatformConfigBP(
226227
name="platform_config",

bases/renku_data_services/data_api/dependencies.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,6 @@ def from_env(cls) -> DependencyManager:
291291
)
292292
if config.builds.enabled:
293293
k8s_db_cache = K8sDbCache(config.db.async_session_maker)
294-
default_kubeconfig = KubeConfigEnv()
295294
shipwright_client = ShipwrightClient(
296295
client=K8sClusterClientsPool(
297296
lambda: get_clusters(
@@ -342,12 +341,21 @@ def from_env(cls) -> DependencyManager:
342341
group_repo=group_repo,
343342
search_updates_repo=search_updates_repo,
344343
)
344+
345+
git_repositories_repo = GitRepositoriesRepository(
346+
session_maker=config.db.async_session_maker,
347+
oauth_client_factory=oauth_http_client_factory,
348+
internal_gitlab_url=config.gitlab_url,
349+
enable_internal_gitlab=config.enable_internal_gitlab,
350+
)
351+
345352
session_repo = SessionRepository(
346353
session_maker=config.db.async_session_maker,
347354
project_authz=authz,
348355
resource_pools=rp_repo,
349356
shipwright_client=shipwright_client,
350357
builds_config=config.builds,
358+
git_repositories_repo=git_repositories_repo,
351359
)
352360
project_migration_repo = ProjectMigrationRepository(
353361
session_maker=config.db.async_session_maker,
@@ -378,12 +386,6 @@ def from_env(cls) -> DependencyManager:
378386
user_repo=kc_user_repo,
379387
secret_service_public_key=config.secrets.public_key,
380388
)
381-
git_repositories_repo = GitRepositoriesRepository(
382-
session_maker=config.db.async_session_maker,
383-
oauth_client_factory=oauth_http_client_factory,
384-
internal_gitlab_url=config.gitlab_url,
385-
enable_internal_gitlab=config.enable_internal_gitlab,
386-
)
387389
platform_repo = PlatformRepository(
388390
session_maker=config.db.async_session_maker,
389391
)

components/renku_data_services/authn/dummy.py

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class DummyAuthenticator:
4646
async def authenticate(self, access_token: str, request: Request) -> base_models.APIUser:
4747
"""Indicates whether the user has successfully logged in."""
4848
access_token = request.headers.get(self.token_field) or ""
49+
4950
if not access_token or len(access_token) == 0:
5051
# Try to get an anonymous user ID if the validation of keycloak credentials failed
5152
anon_id = request.headers.get(self.anon_id_header_key)
@@ -60,23 +61,17 @@ async def authenticate(self, access_token: str, request: Request) -> base_models
6061
with contextlib.suppress(Exception):
6162
user_props = json.loads(access_token)
6263

63-
is_set = bool(
64-
user_props.get("id")
65-
or user_props.get("full_name")
66-
or user_props.get("is_admin") is not None
67-
or user_props.get("first_name")
68-
or user_props.get("last_name")
69-
or user_props.get("email")
70-
)
64+
if user_props.get("id") is None:
65+
return base_models.APIUser()
7166

72-
return base_models.APIUser(
67+
return base_models.AuthenticatedAPIUser(
7368
is_admin=user_props.get("is_admin", False),
74-
id=user_props.get("id", "some-id") if is_set else None,
69+
id=user_props.get("id", ""),
7570
access_token=access_token,
76-
first_name=user_props.get("first_name", "John") if is_set else None,
77-
last_name=user_props.get("last_name", "Doe") if is_set else None,
78-
email=user_props.get("email", "john.doe@gmail.com") if is_set else None,
79-
full_name=user_props.get("full_name", "John Doe") if is_set else None,
71+
first_name=user_props.get("first_name"),
72+
last_name=user_props.get("last_name"),
73+
email=user_props.get("email", ""),
74+
full_name=user_props.get("full_name"),
8075
refresh_token=request.headers.get("Renku-Auth-Refresh-Token"),
8176
roles=user_props.get("roles", []),
8277
)

components/renku_data_services/capacity_reservation/apispec_base.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,17 @@
22

33
from typing import Any
44

5-
from pydantic import BaseModel, field_validator
5+
from pydantic import BaseModel, ConfigDict, field_validator
66
from ulid import ULID
77

88

99
class BaseAPISpec(BaseModel):
1010
"""Base API specification."""
1111

12-
class Config:
13-
"""Enables orm mode for pydantic."""
14-
15-
from_attributes = True
12+
# Enables orm mode for pydantic.
13+
model_config = ConfigDict(
14+
from_attributes=True,
15+
)
1616

1717
@field_validator("*", mode="before", check_fields=False)
1818
@classmethod
Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
"""Base models for API specifications."""
22

3-
from pydantic import BaseModel, Field, field_validator
3+
from pydantic import BaseModel, ConfigDict, Field, field_validator
44
from ulid import ULID
55

66

77
class BaseAPISpec(BaseModel):
88
"""Base API specification."""
99

10-
class Config:
11-
"""Enables orm mode for pydantic."""
12-
13-
from_attributes = True
10+
model_config = ConfigDict(
11+
# Enables orm mode for pydantic."""
12+
from_attributes=True,
1413
# NOTE: By default the pydantic library does not use python for regex but a rust crate
1514
# this rust crate does not support lookahead regex syntax but we need it in this component
16-
regex_engine = "python-re"
15+
regex_engine="python-re",
16+
)
1717

1818
@field_validator("id", mode="before", check_fields=False)
1919
@classmethod
@@ -25,20 +25,14 @@ def serialize_id(cls, id: str | ULID) -> str:
2525
class AuthorizeParams(BaseAPISpec):
2626
"""The schema for the query parameters used in the authorize request."""
2727

28-
class Config:
29-
"""Configuration."""
30-
31-
extra = "ignore"
28+
model_config = ConfigDict(extra="ignore")
3229

3330
next_url: str = Field(default="")
3431

3532

3633
class CallbackParams(BaseAPISpec):
3734
"""The schema for the query parameters used in the authorize callback request."""
3835

39-
class Config:
40-
"""Configuration."""
41-
42-
extra = "ignore"
36+
model_config = ConfigDict(extra="ignore")
4337

4438
state: str = Field(default="")

components/renku_data_services/crc/apispec.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,8 @@ class UsersUserIdResourcePoolsGetParametersQuery(BaseAPISpec):
250250
user_resource_params: Optional[UserResourceParams] = None
251251

252252

253+
config_name_pattern = r"^[a-zA-Z0-9._-]+(\.yaml)?$"
254+
253255
class Cluster(BaseAPISpec):
254256
model_config = ConfigDict(
255257
extra="forbid",
@@ -264,7 +266,7 @@ class Cluster(BaseAPISpec):
264266
...,
265267
description="The name of the Kubernetes configuration to use to connect to the remote cluster. This is currently used to find a file named `<KubeConfigRoot>/<config_name>`.\n\nThis configuration is expected to have a default namespace defined. It will be used for all remote operations requiring a namespace, as well for namespaced objects.\n",
266268
examples=["a-remote-cluster.yaml"],
267-
pattern="^[a-zA-Z0-9._-]+[.]yaml$",
269+
pattern=config_name_pattern,
268270
)
269271
session_protocol: Protocol
270272
session_host: str = Field(
@@ -301,7 +303,7 @@ class ClusterPatch(BaseAPISpec):
301303
None,
302304
description="The name of the Kubernetes configuration to use to connect to the remote cluster. This is currently used to find a file named `<KubeConfigRoot>/<config_name>`.\n\nThis configuration is expected to have a default namespace defined. It will be used for all remote operations requiring a namespace, as well for namespaced objects.\n",
303305
examples=["a-remote-cluster.yaml"],
304-
pattern="^[a-zA-Z0-9._-]+[.]yaml$",
306+
pattern=config_name_pattern,
305307
)
306308
session_protocol: Optional[Protocol] = None
307309
session_host: Optional[str] = Field(
@@ -338,7 +340,7 @@ class ClusterWithId(BaseAPISpec):
338340
...,
339341
description="The name of the Kubernetes configuration to use to connect to the remote cluster. This is currently used to find a file named `<KubeConfigRoot>/<config_name>`.\n\nThis configuration is expected to have a default namespace defined. It will be used for all remote operations requiring a namespace, as well for namespaced objects.\n",
340342
examples=["a-remote-cluster.yaml"],
341-
pattern="^[a-zA-Z0-9._-]+[.]yaml$",
343+
pattern=config_name_pattern,
342344
)
343345
id: str = Field(
344346
...,

components/renku_data_services/crc/apispec_base.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from pathlib import PurePosixPath
44
from typing import Any
55

6-
from pydantic import BaseModel, field_validator
6+
from pydantic import BaseModel, ConfigDict, field_validator
77
from ulid import ULID
88

99
from renku_data_services.session import models
@@ -12,10 +12,10 @@
1212
class BaseAPISpec(BaseModel):
1313
"""Base API specification."""
1414

15-
class Config:
16-
"""Enables orm mode for pydantic."""
17-
18-
from_attributes = True
15+
# Enables orm mode for pydantic.
16+
model_config = ConfigDict(
17+
from_attributes=True,
18+
)
1919

2020
@field_validator("*", mode="before", check_fields=False)
2121
@classmethod

components/renku_data_services/crc/server_options.py

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from collections.abc import Generator
99
from typing import Any, Union
1010

11-
from pydantic import BaseModel, ByteSize, Field, validator
11+
from pydantic import BaseModel, ByteSize, ConfigDict, Field, validator
1212

1313
from renku_data_services.crc import models
1414
from renku_data_services.crc.constants import DEFAULT_RUNTIME_PLATFORM
@@ -28,17 +28,13 @@ class ServerOptionsDefaults(BaseModel):
2828
disk_request: ByteSize
2929
gpu_request: int = Field(ge=0, default=0)
3030

31-
class Config:
32-
"""Configuration."""
33-
34-
extra = "ignore"
31+
model_config = ConfigDict(extra="ignore")
3532

3633

3734
class _ServerOptionsCpu(BaseModel):
3835
options: list[float] = Field(min_length=1)
3936

40-
class Config:
41-
extra = "ignore"
37+
model_config = ConfigDict(extra="ignore")
4238

4339
@validator("options", pre=False, each_item=True)
4440
def greater_than_zero(cls, val: Union[float, int]) -> Union[float, int]:
@@ -61,8 +57,7 @@ def greater_than_or_equal_to_zero(cls, v: int) -> int:
6157
class _ServerOptionsBytes(BaseModel):
6258
options: list[ByteSize] = Field(min_length=1)
6359

64-
class Config:
65-
extra = "ignore"
60+
model_config = ConfigDict(extra="ignore")
6661

6762
@validator("options", pre=True)
6863
def convert_units(cls, vals: list[str]) -> list[str]:
@@ -84,10 +79,7 @@ class ServerOptions(BaseModel):
8479
disk_request: _ServerOptionsBytes
8580
gpu_request: _ServerOptionsGpu = Field(default_factory=lambda: _ServerOptionsGpu(options=[0]))
8681

87-
class Config:
88-
"""Configuration."""
89-
90-
extra = "ignore"
82+
model_config = ConfigDict(extra="ignore")
9183

9284
def find_largest_attribute(self) -> str:
9385
"""Find the attribute with the largest number of choices."""

components/renku_data_services/data_connectors/apispec_base.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
"""Base models for API specifications."""
22

3-
from pydantic import BaseModel, field_validator
3+
from pydantic import BaseModel, ConfigDict, field_validator
44
from ulid import ULID
55

66

77
class BaseAPISpec(BaseModel):
88
"""Base API specification."""
99

10-
class Config:
11-
"""Enables orm mode for pydantic."""
12-
13-
from_attributes = True
10+
model_config = ConfigDict(
11+
# Enables orm mode for pydantic."""
12+
from_attributes=True,
1413
# NOTE: By default the pydantic library does not use python for regex but a rust crate
1514
# this rust crate does not support lookahead regex syntax but we need it in this component
16-
regex_engine = "python-re"
15+
regex_engine="python-re",
16+
)
1717

1818
@field_validator("id", mode="before", check_fields=False)
1919
@classmethod

components/renku_data_services/k8s/clients.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,13 +346,18 @@ async def create(self, obj: K8sObject, refresh: bool) -> K8sObject:
346346
if not obj.namespaced():
347347
raise NotImplementedError("Caching of cluster scoped K8s resources is not supported")
348348
await self.__cache.upsert(obj)
349+
349350
try:
350351
obj = await super().create(obj, refresh)
351352
except:
352353
# if there was an error creating the k8s object, we delete it from the db again to not have ghost entries
353354
if obj.gvk in self.__kinds_to_cache:
354355
await self.__cache.delete(obj)
355356
raise
357+
358+
if refresh and obj.gvk in self.__kinds_to_cache:
359+
await self.__cache.upsert(obj)
360+
356361
return obj
357362

358363
async def patch(self, meta: K8sObjectMeta, patch: K8sPatches) -> K8sObject:

0 commit comments

Comments
 (0)