Skip to content

Commit 49eb81a

Browse files
authored
feat: cluster default tls cert in sessions (#1183)
1 parent 9c2cbb4 commit 49eb81a

12 files changed

Lines changed: 356 additions & 92 deletions

File tree

components/renku_data_services/crc/api.spec.yaml

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -996,7 +996,19 @@ components:
996996
$ref: "#/components/schemas/StorageClassName"
997997
service_account_name:
998998
$ref: "#/components/schemas/K8sResourceName"
999-
required: [ "name", "config_name", "session_protocol", "session_host", "session_port", "session_path", "session_ingress_annotations", "session_tls_secret_name" ]
999+
session_ingress_use_default_cluster_tls_cert:
1000+
type: boolean
1001+
default: false
1002+
required:
1003+
[
1004+
"name",
1005+
"config_name",
1006+
"session_protocol",
1007+
"session_host",
1008+
"session_port",
1009+
"session_path",
1010+
"session_ingress_annotations",
1011+
]
10001012
ClusterPatch:
10011013
type: object
10021014
additionalProperties: false
@@ -1018,11 +1030,15 @@ components:
10181030
session_ingress_annotations:
10191031
$ref: "#/components/schemas/IngressAnnotations"
10201032
session_tls_secret_name:
1021-
$ref: "#/components/schemas/TlsSecretName"
1033+
allOf:
1034+
- $ref: "#/components/schemas/TlsSecretName"
1035+
- description: 'If you want to remove the secret name pass in an empty string ""'
10221036
session_storage_class:
10231037
$ref: "#/components/schemas/StorageClassName"
10241038
service_account_name:
10251039
$ref: "#/components/schemas/K8sResourceNamePatch"
1040+
session_ingress_use_default_cluster_tls_cert:
1041+
type: boolean
10261042
ClusterWithId:
10271043
type: object
10281044
additionalProperties: false
@@ -1051,7 +1067,20 @@ components:
10511067
$ref: "#/components/schemas/StorageClassName"
10521068
service_account_name:
10531069
type: string
1054-
required: [ "name", "config_name", "session_protocol", "session_host", "session_port", "session_path", "session_ingress_annotations", "session_tls_secret_name", "id" ]
1070+
session_ingress_use_default_cluster_tls_cert:
1071+
type: boolean
1072+
required:
1073+
[
1074+
"name",
1075+
"config_name",
1076+
"session_protocol",
1077+
"session_host",
1078+
"session_port",
1079+
"session_path",
1080+
"session_ingress_annotations",
1081+
"id",
1082+
"session_ingress_use_default_cluster_tls_cert",
1083+
]
10551084
ClustersWithId:
10561085
type: array
10571086
items:

components/renku_data_services/crc/apispec.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# generated by datamodel-codegen:
22
# filename: api.spec.yaml
3-
# timestamp: 2025-12-12T08:13:17+00:00
3+
# timestamp: 2026-01-20T15:22:21+00:00
44

55
from __future__ import annotations
66

@@ -238,7 +238,7 @@ class Cluster(BaseAPISpec):
238238
session_path: str
239239
session_ingress_class_name: Optional[str] = Field(None, max_length=256)
240240
session_ingress_annotations: IngressAnnotations
241-
session_tls_secret_name: str = Field(..., max_length=256)
241+
session_tls_secret_name: Optional[str] = Field(None, max_length=256)
242242
session_storage_class: Optional[str] = Field(None, max_length=256)
243243
service_account_name: Optional[str] = Field(
244244
None,
@@ -248,6 +248,7 @@ class Cluster(BaseAPISpec):
248248
min_length=1,
249249
pattern="^[a-z0-9][a-z0-9-]*[a-z0-9]$",
250250
)
251+
session_ingress_use_default_cluster_tls_cert: bool = False
251252

252253

253254
class ClusterPatch(BaseAPISpec):
@@ -284,6 +285,7 @@ class ClusterPatch(BaseAPISpec):
284285
min_length=0,
285286
pattern="^[a-z0-9][a-z0-9-]*[a-z0-9]$",
286287
)
288+
session_ingress_use_default_cluster_tls_cert: Optional[bool] = None
287289

288290

289291
class ClusterWithId(BaseAPISpec):
@@ -317,9 +319,10 @@ class ClusterWithId(BaseAPISpec):
317319
session_path: str
318320
session_ingress_class_name: Optional[str] = Field(None, max_length=256)
319321
session_ingress_annotations: IngressAnnotations
320-
session_tls_secret_name: str = Field(..., max_length=256)
322+
session_tls_secret_name: Optional[str] = Field(None, max_length=256)
321323
session_storage_class: Optional[str] = Field(None, max_length=256)
322324
service_account_name: Optional[str] = None
325+
session_ingress_use_default_cluster_tls_cert: bool
323326

324327

325328
class ClustersWithId(RootModel[List[ClusterWithId]]):
@@ -824,7 +827,7 @@ class ResourcePoolPatch(BaseAPISpec):
824827
)
825828
hibernation_warning_period: Optional[int] = Field(
826829
None,
827-
description="A duration in seconds, lower than `HibernationThreshold`,\nwhich indicates when to let the user know about a session that\nis going to be expired. If `0` is specified, some default\nvalue will be chosen. If no `HibernationThreshold` is defined,\nthis value is ignored.\n",
830+
description="A duration in seconds, lower than `HibernationThreshold`,\nwhich indicates when to let the user know about a session that\nis going to be hibernated. If `0` is specified, some default\nvalue will be chosen. If no `HibernationThreshold` is defined,\nthis value is ignored.\n",
828831
ge=0,
829832
le=2147483647,
830833
)
@@ -878,7 +881,7 @@ class ResourcePool(BaseAPISpec):
878881
)
879882
hibernation_warning_period: Optional[int] = Field(
880883
None,
881-
description="A duration in seconds, lower than `HibernationThreshold`,\nwhich indicates when to let the user know about a session that\nis going to be expired. If `0` is specified, some default\nvalue will be chosen. If no `HibernationThreshold` is defined,\nthis value is ignored.\n",
884+
description="A duration in seconds, lower than `HibernationThreshold`,\nwhich indicates when to let the user know about a session that\nis going to be hibernated. If `0` is specified, some default\nvalue will be chosen. If no `HibernationThreshold` is defined,\nthis value is ignored.\n",
882885
ge=0,
883886
le=2147483647,
884887
)
@@ -958,7 +961,7 @@ class ResourcePoolPut(BaseAPISpec):
958961
)
959962
hibernation_warning_period: Optional[int] = Field(
960963
None,
961-
description="A duration in seconds, lower than `HibernationThreshold`,\nwhich indicates when to let the user know about a session that\nis going to be expired. If `0` is specified, some default\nvalue will be chosen. If no `HibernationThreshold` is defined,\nthis value is ignored.\n",
964+
description="A duration in seconds, lower than `HibernationThreshold`,\nwhich indicates when to let the user know about a session that\nis going to be hibernated. If `0` is specified, some default\nvalue will be chosen. If no `HibernationThreshold` is defined,\nthis value is ignored.\n",
962965
ge=0,
963966
le=2147483647,
964967
)
@@ -1018,7 +1021,7 @@ class ResourcePoolWithId(BaseAPISpec):
10181021
)
10191022
hibernation_warning_period: Optional[int] = Field(
10201023
None,
1021-
description="A duration in seconds, lower than `HibernationThreshold`,\nwhich indicates when to let the user know about a session that\nis going to be expired. If `0` is specified, some default\nvalue will be chosen. If no `HibernationThreshold` is defined,\nthis value is ignored.\n",
1024+
description="A duration in seconds, lower than `HibernationThreshold`,\nwhich indicates when to let the user know about a session that\nis going to be hibernated. If `0` is specified, some default\nvalue will be chosen. If no `HibernationThreshold` is defined,\nthis value is ignored.\n",
10221025
ge=0,
10231026
le=2147483647,
10241027
)
@@ -1072,7 +1075,7 @@ class ResourcePoolWithIdFiltered(BaseAPISpec):
10721075
)
10731076
hibernation_warning_period: Optional[int] = Field(
10741077
None,
1075-
description="A duration in seconds, lower than `HibernationThreshold`,\nwhich indicates when to let the user know about a session that\nis going to be expired. If `0` is specified, some default\nvalue will be chosen. If no `HibernationThreshold` is defined,\nthis value is ignored.\n",
1078+
description="A duration in seconds, lower than `HibernationThreshold`,\nwhich indicates when to let the user know about a session that\nis going to be hibernated. If `0` is specified, some default\nvalue will be chosen. If no `HibernationThreshold` is defined,\nthis value is ignored.\n",
10761079
ge=0,
10771080
le=2147483647,
10781081
)

components/renku_data_services/crc/core.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,24 @@ def validate_resource_pool_update(existing: models.ResourcePool, update: models.
388388

389389
def validate_cluster(body: apispec.Cluster) -> models.ClusterSettings:
390390
"""Convert a REST API Cluster object to a model Cluster object."""
391+
match (
392+
body.session_protocol,
393+
body.session_tls_secret_name,
394+
body.session_ingress_use_default_cluster_tls_cert,
395+
):
396+
case (apispec.Protocol.https, None, False) | (apispec.Protocol.https, "", False):
397+
raise errors.ValidationError(
398+
message=f"You have indicated that cluster {body.name} should use HTTPS for the ingress "
399+
"but neither the TLS secret name nor the flag that indicates that the default cluster TLS secret "
400+
"should be used are set. Please set only one of these two options."
401+
)
402+
case (apispec.Protocol.https, str(), True) if len(body.session_tls_secret_name) > 0:
403+
raise errors.ValidationError(
404+
message=f"You have indicated that cluster {body.name} should use HTTPS for the ingress "
405+
"but you have set both the TLS secret name and the flag that indicates that the default "
406+
"cluster TLS secret should be used. "
407+
"Please set only one of these two options."
408+
)
391409
return models.ClusterSettings(
392410
name=body.name,
393411
config_name=body.config_name,
@@ -406,6 +424,15 @@ def validate_cluster(body: apispec.Cluster) -> models.ClusterSettings:
406424
def validate_cluster_patch(patch: apispec.ClusterPatch) -> models.ClusterPatch:
407425
"""Convert a REST API Cluster object patch to a model Cluster object."""
408426

427+
if patch.session_ingress_use_default_cluster_tls_cert and patch.session_tls_secret_name:
428+
raise errors.ValidationError(
429+
message="Setting both the TLS secret name and indicating you want to use the default cluster TLS cert "
430+
"at the same time is not allowed."
431+
)
432+
433+
session_tls_secret_name: None | ResetType | str = patch.session_tls_secret_name
434+
if isinstance(patch.session_tls_secret_name, str) and len(patch.session_tls_secret_name) == 0:
435+
session_tls_secret_name = RESET
409436
return models.ClusterPatch(
410437
name=patch.name,
411438
config_name=patch.config_name,
@@ -419,9 +446,10 @@ def validate_cluster_patch(patch: apispec.ClusterPatch) -> models.ClusterPatch:
419446
session_ingress_annotations=patch.session_ingress_annotations.model_dump()
420447
if patch.session_ingress_annotations is not None
421448
else None,
422-
session_tls_secret_name=patch.session_tls_secret_name,
449+
session_tls_secret_name=session_tls_secret_name,
423450
session_storage_class=patch.session_storage_class,
424451
service_account_name=patch.service_account_name,
452+
session_ingress_use_default_cluster_tls_cert=patch.session_ingress_use_default_cluster_tls_cert,
425453
)
426454

427455

components/renku_data_services/crc/db.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1043,6 +1043,36 @@ async def update(self, api_user: base_models.APIUser, cluster: ClusterPatch, clu
10431043
if saved_cluster is None:
10441044
raise errors.MissingResourceError(message=f"Cluster definition id='{cluster_id}' does not exist.")
10451045

1046+
_new_session_protocol = cluster.session_protocol or saved_cluster.session_protocol
1047+
if isinstance(_new_session_protocol, SessionProtocol):
1048+
new_session_protocol = _new_session_protocol.value.lower()
1049+
else:
1050+
new_session_protocol = _new_session_protocol.lower()
1051+
new_session_tls_secret_name = cluster.session_tls_secret_name or saved_cluster.session_tls_secret_name
1052+
new_session_ingress_use_default_cluster_tls_cert = (
1053+
cluster.session_ingress_use_default_cluster_tls_cert
1054+
or saved_cluster.session_ingress_use_default_cluster_tls_cert
1055+
)
1056+
match (
1057+
new_session_protocol,
1058+
new_session_tls_secret_name,
1059+
new_session_ingress_use_default_cluster_tls_cert,
1060+
):
1061+
case ("https", str(), True) if new_session_tls_secret_name:
1062+
raise errors.ValidationError(
1063+
message=f"You are patching cluster {saved_cluster.id} which uses or will use https but"
1064+
"you are using or will use both the TLS secret name and the flag "
1065+
"that indicates that the cluster default TLS secret should be used.",
1066+
detail="Please only use only one of the two configuration options.",
1067+
)
1068+
case ("https", ResetType.Reset, False) | ("https", "", False):
1069+
raise errors.ValidationError(
1070+
message=f"You are patching cluster {saved_cluster.id} which uses or will use https but"
1071+
"you are also removing both the TLS secret name and the flag "
1072+
"that indicates that the cluster default TLS secret should be used.",
1073+
detail="Please only use only one of the two configuration options.",
1074+
)
1075+
10461076
for key, value in asdict(cluster).items():
10471077
match key, value:
10481078
case "session_protocol", SessionProtocol():
@@ -1054,6 +1084,10 @@ async def update(self, api_user: base_models.APIUser, cluster: ClusterPatch, clu
10541084
case "service_account_name", "":
10551085
# If we received an empty string in the service account name, set it back to None.
10561086
setattr(saved_cluster, key, None)
1087+
case "session_tls_secret_name", "":
1088+
setattr(saved_cluster, key, None)
1089+
case "session_tls_secret_name", ResetType.Reset:
1090+
setattr(saved_cluster, key, None)
10571091
case _, None:
10581092
# Do not modify a value which has not been set in the patch
10591093
pass

components/renku_data_services/crc/models.py

Lines changed: 5 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
from renku_data_services import errors
1111
from renku_data_services.base_models import ResetType
1212
from renku_data_services.k8s.constants import DEFAULT_K8S_CLUSTER, ClusterId
13-
from renku_data_services.notebooks.cr_amalthea_session import TlsSecret
1413

1514

1615
class ResourcesProtocol(Protocol):
@@ -196,9 +195,10 @@ class ClusterPatch:
196195
session_path: str | None
197196
session_ingress_class_name: str | None
198197
session_ingress_annotations: dict[str, Any] | None
199-
session_tls_secret_name: str | None
198+
session_tls_secret_name: str | None | ResetType
200199
session_storage_class: str | None
201200
service_account_name: str | None
201+
session_ingress_use_default_cluster_tls_cert: bool | None = False
202202

203203

204204
@dataclass(frozen=True, eq=True, kw_only=True)
@@ -213,9 +213,10 @@ class ClusterSettings:
213213
session_path: str
214214
session_ingress_class_name: str | None = None
215215
session_ingress_annotations: dict[str, str]
216-
session_tls_secret_name: str
216+
session_tls_secret_name: str | None
217217
session_storage_class: str | None
218218
service_account_name: str | None = None
219+
session_ingress_use_default_cluster_tls_cert: bool = False
219220

220221
def to_cluster_patch(self) -> ClusterPatch:
221222
"""Convert to ClusterPatch."""
@@ -232,49 +233,14 @@ def to_cluster_patch(self) -> ClusterPatch:
232233
session_tls_secret_name=self.session_tls_secret_name,
233234
session_storage_class=self.session_storage_class,
234235
service_account_name=self.service_account_name,
236+
session_ingress_use_default_cluster_tls_cert=self.session_ingress_use_default_cluster_tls_cert,
235237
)
236238

237239
def get_storage_class(self) -> str | None:
238240
"""Get the default storage class for the cluster."""
239241

240242
return self.session_storage_class
241243

242-
def get_ingress_parameters(
243-
self, server_name: str
244-
) -> tuple[str, str, str, str, TlsSecret | None, str | None, dict[str, str]]:
245-
"""Returns the ingress parameters of the cluster."""
246-
247-
host = self.session_host
248-
base_server_path = f"{self.session_path}/{server_name}"
249-
if self.session_port in [80, 443]:
250-
# No need to specify the port in these cases. If we specify the port on https or http
251-
# when it is the usual port then the URL callbacks for authentication do not work.
252-
# I.e. if the callback is registered as https://some.host/link it will not work when a redirect
253-
# like https://some.host:443/link is used.
254-
base_server_url = f"{self.session_protocol.value}://{host}{base_server_path}"
255-
else:
256-
base_server_url = f"{self.session_protocol.value}://{host}:{self.session_port}{base_server_path}"
257-
base_server_https_url = base_server_url
258-
ingress_class_name = self.session_ingress_class_name
259-
ingress_annotations = self.session_ingress_annotations
260-
261-
if ingress_class_name is None:
262-
ingress_class_name = ingress_annotations.get("kubernetes.io/ingress.class")
263-
264-
tls_secret = (
265-
None if self.session_tls_secret_name is None else TlsSecret(adopt=False, name=self.session_tls_secret_name)
266-
)
267-
268-
return (
269-
base_server_path,
270-
base_server_url,
271-
base_server_https_url,
272-
host,
273-
tls_secret,
274-
ingress_class_name,
275-
ingress_annotations,
276-
)
277-
278244

279245
@dataclass(frozen=True, eq=True, kw_only=True)
280246
class SavedClusterSettings(ClusterSettings):

0 commit comments

Comments
 (0)