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

SwissDataScienceCenter / renku-data-services / 12318338392

13 Dec 2024 03:21PM UTC coverage: 86.388% (-0.001%) from 86.389%
12318338392

Pull #572

github

web-flow
Merge 0eb4f5dce into 9cb5d8146
Pull Request #572: feat: manage v1 sessions

17 of 32 new or added lines in 5 files covered. (53.13%)

4 existing lines in 4 files now uncovered.

14641 of 16948 relevant lines covered (86.39%)

1.52 hits per line

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

61.7
/components/renku_data_services/notebooks/util/kubernetes_.py
1
#
2
# Copyright 2019 - Swiss Data Science Center (SDSC)
3
# A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and
4
# Eidgenössische Technische Hochschule Zürich (ETHZ).
5
#
6
# Licensed under the Apache License, Version 2.0 (the "License");
7
# you may not use this file except in compliance with the License.
8
# You may obtain a copy of the License at
9
#
10
#     http://www.apache.org/licenses/LICENSE-2.0
11
#
12
# Unless required by applicable law or agreed to in writing, software
13
# distributed under the License is distributed on an "AS IS" BASIS,
14
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
# See the License for the specific language governing permissions and
16
# limitations under the License.
17
"""Kubernetes helper functions."""
18

19
from __future__ import annotations
2✔
20

21
import re
2✔
22
from enum import StrEnum
2✔
23
from hashlib import md5
2✔
24
from typing import Any, TypeAlias, cast
2✔
25

26
import escapism
2✔
27
from box import Box
2✔
28

29
from renku_data_services.base_models.core import AnonymousAPIUser, AuthenticatedAPIUser, Slug
2✔
30
from renku_data_services.notebooks.crs import Patch, PatchType
2✔
31

32

33
def renku_1_make_server_name(safe_username: str, namespace: str, project: str, branch: str, commit_sha: str) -> str:
2✔
34
    """Form a unique server name for Renku 1.0 sessions.
35

36
    This is used in naming all the k8s resources created by amalthea.
37
    """
38
    server_string_for_hashing = f"{safe_username}-{namespace}-{project}-{branch}-{commit_sha}"
1✔
39
    server_hash = md5(server_string_for_hashing.encode(), usedforsecurity=False).hexdigest().lower()
1✔
40
    prefix = _make_server_name_prefix(safe_username)
1✔
41
    # NOTE: A K8s object name can only contain lowercase alphanumeric characters, hyphens, or dots.
42
    # Must be less than 253 characters long and start and end with an alphanumeric.
43
    # NOTE: We use server name as a label value, so, server name must be less than 63 characters.
44
    # NOTE: Amalthea adds 11 characters to the server name in a label, so we have only
45
    # 52 characters available.
46
    # !NOTE: For now we limit the server name to 42 characters.
47
    # NOTE: This is 12 + 1 + 20 + 1 + 8 = 42 characters
48
    return "{prefix}-{project}-{hash}".format(
1✔
49
        prefix=prefix[:12],
50
        project=escapism.escape(project, escape_char="-")[:20].lower(),
51
        hash=server_hash[:8],
52
    )
53

54

55
def renku_2_make_server_name(user: AuthenticatedAPIUser | AnonymousAPIUser, project_id: str, launcher_id: str) -> str:
2✔
56
    """Form a unique server name for Renku 2.0 sessions.
57

58
    This is used in naming all the k8s resources created by amalthea.
59
    """
60
    safe_username = Slug.from_user(user.email, user.first_name, user.last_name, user.id).value
×
61
    safe_username = safe_username.lower()
×
62
    safe_username = re.sub(r"[^a-z0-9-]", "-", safe_username)
×
63
    prefix = _make_server_name_prefix(safe_username)
×
64
    server_string_for_hashing = f"{user.id}-{project_id}-{launcher_id}"
×
65
    server_hash = md5(server_string_for_hashing.encode(), usedforsecurity=False).hexdigest().lower()
×
66
    # NOTE: A K8s object name can only contain lowercase alphanumeric characters, hyphens, or dots.
67
    # Must be no more than 63 characters because the name is used to create a k8s Service and Services
68
    # have more restrictions for their names beacuse their names have to make a valid hostname.
69
    # NOTE: We use server name as a label value, so, server name must be less than 63 characters.
70
    # !NOTE: For now we limit the server name to a max of 25 characters.
71
    # NOTE: This is 12 + 1 + 12 = 25 characters
72
    return f"{prefix[:12]}-{server_hash[:12]}"
×
73

74

75
def find_env_var(env_vars: list[Box], env_name: str) -> tuple[int, Box] | None:
2✔
76
    """Find the index and value of a specific environment variable by name from a Kubernetes container."""
NEW
77
    filtered = (env_var for env_var in enumerate(env_vars) if env_var[1].name == env_name)
×
NEW
78
    return next(filtered, None)
×
79

80

81
def _make_server_name_prefix(safe_username: str) -> str:
2✔
82
    prefix = ""
1✔
83
    if not safe_username[0].isalpha() or not safe_username[0].isascii():
1✔
84
        # NOTE: Username starts with an invalid character. This has to be modified because a
85
        # k8s service object cannot start with anything other than a lowercase alphabet character.
86
        # NOTE: We do not have worry about collisions with already existing servers from older
87
        # versions because the server name includes the hash of the original username, so the hash
88
        # would be different because the original username differs between someone whose username
89
        # is for example 7User vs. n7User.
90
        prefix = "n"
×
91

92
    prefix = f"{prefix}{safe_username}"
1✔
93
    return prefix
1✔
94

95

96
JsonPatch: TypeAlias = list[dict[str, Any]]
2✔
97
MergePatch: TypeAlias = dict[str, Any]
2✔
98

99

100
class PatchKind(StrEnum):
2✔
101
    """Content types for different json patches."""
102

103
    json: str = "application/json-patch+json"
2✔
104
    merge: str = "application/merge-patch+json"
2✔
105

106

107
def find_container(patches: list[Patch], container_name: str) -> dict[str, Any] | None:
2✔
108
    """Find the json patch corresponding a given container."""
109
    # rfc 7386 patches are dictionaries, i.e. merge patch or json merge patch
110
    # rfc 6902 patches are lists, i.e. json patch
111
    for patch_obj in patches:
1✔
112
        if patch_obj.type != PatchType.application_json_patch_json or not isinstance(patch_obj.patch, list):
×
113
            continue
×
114
        for p in patch_obj.patch:
×
115
            if not isinstance(p, dict):
×
116
                continue
×
117
            p = cast(dict[str, Any], p)
×
118
            if (
×
119
                p.get("op") == "add"
120
                and p.get("path") == "/statefulset/spec/template/spec/containers/-"
121
                and p.get("value", {}).get("name") == container_name
122
            ):
123
                return p
×
124
    return None
1✔
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