Skip to content

Commit 1c9204b

Browse files
authored
fix: patching wrong environment variables when resuming (#1923)
Fixes #1921. Reported by a user. The wrong environment variable was patches when the session was hibernated and the access tokens were expired.
1 parent 625a0eb commit 1c9204b

File tree

3 files changed

+131
-68
lines changed

3 files changed

+131
-68
lines changed

renku_notebooks/api/classes/k8s_client.py

Lines changed: 27 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -246,18 +246,8 @@ def patch_image_pull_secret(self, server_name: str, gitlab_token: GitlabToken):
246246
patch,
247247
)
248248

249-
def patch_statefulset_tokens(self, name: str, renku_tokens: RenkuTokens):
250-
"""Patch the Renku and Gitlab access tokens that are used in the session statefulset."""
251-
try:
252-
sts = self._apps_v1.read_namespaced_stateful_set(name, self.namespace)
253-
except ApiException as err:
254-
if err.status == 404:
255-
# NOTE: It can happen potentially that another request or something else
256-
# deleted the session as this request was going on, in this case we ignore
257-
# the missing statefulset
258-
return
259-
raise
260-
249+
@staticmethod
250+
def _get_statefulset_token_patches(sts: client.V1StatefulSet, renku_tokens: RenkuTokens) -> list[dict[str, str]]:
261251
containers: list[V1Container] = sts.spec.template.spec.containers
262252
init_containers: list[V1Container] = sts.spec.template.spec.init_containers
263253

@@ -266,15 +256,11 @@ def patch_statefulset_tokens(self, name: str, renku_tokens: RenkuTokens):
266256
(None, None),
267257
)
268258
git_clone_container_index, git_clone_container = next(
269-
((i, c) for i, c in enumerate(init_containers) if c.name == "git-proxy"),
259+
((i, c) for i, c in enumerate(init_containers) if c.name == "git-clone"),
270260
(None, None),
271261
)
272262
secrets_container_index, secrets_container = next(
273-
(
274-
(i, c)
275-
for i, c in enumerate(init_containers)
276-
if c.name == "init-user-secrets"
277-
),
263+
((i, c) for i, c in enumerate(init_containers) if c.name == "init-user-secrets"),
278264
(None, None),
279265
)
280266

@@ -294,16 +280,11 @@ def patch_statefulset_tokens(self, name: str, renku_tokens: RenkuTokens):
294280
else None
295281
)
296282
secrets_renku_access_token_env = (
297-
find_env_var(secrets_container, "RENKU_ACCESS_TOKEN")
298-
if secrets_container is not None
299-
else None
283+
find_env_var(secrets_container, "RENKU_ACCESS_TOKEN") if secrets_container is not None else None
300284
)
301285

302286
patches = list()
303-
if (
304-
git_proxy_container_index is not None
305-
and git_proxy_renku_access_token_env is not None
306-
):
287+
if git_proxy_container_index is not None and git_proxy_renku_access_token_env is not None:
307288
patches.append(
308289
{
309290
"op": "replace",
@@ -314,10 +295,7 @@ def patch_statefulset_tokens(self, name: str, renku_tokens: RenkuTokens):
314295
"value": renku_tokens.access_token,
315296
}
316297
)
317-
if (
318-
git_proxy_container_index is not None
319-
and git_proxy_renku_refresh_token_env is not None
320-
):
298+
if git_proxy_container_index is not None and git_proxy_renku_refresh_token_env is not None:
321299
patches.append(
322300
{
323301
"op": "replace",
@@ -328,35 +306,45 @@ def patch_statefulset_tokens(self, name: str, renku_tokens: RenkuTokens):
328306
"value": renku_tokens.refresh_token,
329307
},
330308
)
331-
if (
332-
git_clone_container_index is not None
333-
and git_clone_renku_access_token_env is not None
334-
):
309+
if git_clone_container_index is not None and git_clone_renku_access_token_env is not None:
335310
patches.append(
336311
{
337312
"op": "replace",
338313
"path": (
339-
f"/spec/template/spec/containers/{git_clone_container_index}"
314+
f"/spec/template/spec/initContainers/{git_clone_container_index}"
340315
f"/env/{git_clone_renku_access_token_env[0]}/value"
341316
),
342317
"value": renku_tokens.access_token,
343318
},
344319
)
345-
if (
346-
secrets_container_index is not None
347-
and secrets_renku_access_token_env is not None
348-
):
320+
if secrets_container_index is not None and secrets_renku_access_token_env is not None:
349321
patches.append(
350322
{
351323
"op": "replace",
352324
"path": (
353-
f"/spec/template/spec/containers/{secrets_container_index}"
325+
f"/spec/template/spec/initContainers/{secrets_container_index}"
354326
f"/env/{secrets_renku_access_token_env[0]}/value"
355327
),
356328
"value": renku_tokens.access_token,
357329
},
358330
)
359331

332+
return patches
333+
334+
def patch_statefulset_tokens(self, name: str, renku_tokens: RenkuTokens):
335+
"""Patch the Renku and Gitlab access tokens that are used in the session statefulset."""
336+
try:
337+
sts = self._apps_v1.read_namespaced_stateful_set(name, self.namespace)
338+
except ApiException as err:
339+
if err.status == 404:
340+
# NOTE: It can happen potentially that another request or something else
341+
# deleted the session as this request was going on, in this case we ignore
342+
# the missing statefulset
343+
return
344+
raise
345+
346+
patches = self._get_statefulset_token_patches(sts, renku_tokens)
347+
360348
if not patches:
361349
return
362350

renku_notebooks/util/kubernetes_.py

Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from typing import Any
2323

2424
import escapism
25-
from kubernetes.client import V1Container
25+
from kubernetes.client import V1Container, V1EnvVarSource
2626

2727

2828
def filter_resources_by_annotations(
@@ -37,10 +37,7 @@ def filter_resources_by_annotations(
3737
def filter_resource(resource):
3838
res = []
3939
for annotation_name in annotations:
40-
res.append(
41-
resource["metadata"]["annotations"].get(annotation_name)
42-
== annotations[annotation_name]
43-
)
40+
res.append(resource["metadata"]["annotations"].get(annotation_name) == annotations[annotation_name])
4441
if len(res) == 0:
4542
return True
4643
else:
@@ -49,16 +46,12 @@ def filter_resource(resource):
4946
return list(filter(filter_resource, resources))
5047

5148

52-
def renku_1_make_server_name(
53-
safe_username: str, namespace: str, project: str, branch: str, commit_sha: str
54-
) -> str:
49+
def renku_1_make_server_name(safe_username: str, namespace: str, project: str, branch: str, commit_sha: str) -> str:
5550
"""Form a unique server name for Renku 1.0 sessions.
5651
5752
This is used in naming all the k8s resources created by amalthea.
5853
"""
59-
server_string_for_hashing = (
60-
f"{safe_username}-{namespace}-{project}-{branch}-{commit_sha}"
61-
)
54+
server_string_for_hashing = f"{safe_username}-{namespace}-{project}-{branch}-{commit_sha}"
6255
server_hash = md5(server_string_for_hashing.encode()).hexdigest().lower()
6356
prefix = _make_server_name_prefix(safe_username)
6457
# NOTE: A K8s object name can only contain lowercase alphanumeric characters, hyphens, or dots.
@@ -75,9 +68,7 @@ def renku_1_make_server_name(
7568
)
7669

7770

78-
def renku_2_make_server_name(
79-
safe_username: str, project_id: str, launcher_id: str
80-
) -> str:
71+
def renku_2_make_server_name(safe_username: str, project_id: str, launcher_id: str) -> str:
8172
"""Form a unique server name for Renku 2.0 sessions.
8273
8374
This is used in naming all the k8s resources created by amalthea.
@@ -95,7 +86,7 @@ def renku_2_make_server_name(
9586
return f"{prefix[:12]}-renku-2-{server_hash[:21]}"
9687

9788

98-
def find_env_var(container: V1Container, env_name: str) -> tuple[int, str] | None:
89+
def find_env_var(container: V1Container, env_name: str) -> tuple[int, str | V1EnvVarSource] | None:
9990
"""Find the index and value of a specific environment variable by name from a Kubernetes container."""
10091
env_var = next(
10192
filter(
@@ -108,16 +99,15 @@ def find_env_var(container: V1Container, env_name: str) -> tuple[int, str] | Non
10899
return None
109100
ind = env_var[0]
110101
val = env_var[1].value
102+
if val is None:
103+
val = env_var[1].value_from
111104
return ind, val
112105

113106

114107
def _make_server_name_prefix(safe_username: str):
115108
safe_username_lowercase = safe_username.lower()
116109
prefix = ""
117-
if (
118-
not safe_username_lowercase[0].isalpha()
119-
or not safe_username_lowercase[0].isascii()
120-
):
110+
if not safe_username_lowercase[0].isalpha() or not safe_username_lowercase[0].isascii():
121111
# NOTE: Username starts with an invalid character. This has to be modified because a
122112
# k8s service object cannot start with anything other than a lowercase alphabet character.
123113
# NOTE: We do not have worry about collisions with already existing servers from older
@@ -130,9 +120,7 @@ def _make_server_name_prefix(safe_username: str):
130120
return prefix
131121

132122

133-
def find_container(
134-
patches: list[dict[str, Any]], container_name: str
135-
) -> dict[str, Any] | None:
123+
def find_container(patches: list[dict[str, Any]], container_name: str) -> dict[str, Any] | None:
136124
"""Find the json patch corresponding a given container."""
137125
for patch_obj in patches:
138126
inner_patches = patch_obj.get("patch", [])

tests/unit/test_k8s_client.py

Lines changed: 94 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,20 @@
11
import pytest
2-
2+
from kubernetes.client import (
3+
V1Container,
4+
V1EnvVar,
5+
V1EnvVarSource,
6+
V1LabelSelector,
7+
V1PodSpec,
8+
V1PodTemplateSpec,
9+
V1StatefulSet,
10+
V1StatefulSetSpec,
11+
)
12+
13+
from renku_notebooks.api.classes.auth import RenkuTokens
314
from renku_notebooks.api.classes.k8s_client import JsServerCache, K8sClient, NamespacedK8sClient
415
from renku_notebooks.errors.intermittent import JSCacheError
516
from renku_notebooks.errors.programming import ProgrammingError
17+
from renku_notebooks.util.kubernetes_ import find_env_var
618

719

820
@pytest.fixture
@@ -37,9 +49,7 @@ def test_list_cache_preference(mock_server_cache, mock_namespaced_client):
3749
renku_ns_client = mock_namespaced_client("renku")
3850
sessions_ns_client = mock_namespaced_client("renku-sessions")
3951
sample_server_manifest = {"metadata": {"labels": {"username": "username"}, "name": "server1"}}
40-
sample_server_manifest_preferred = {
41-
"metadata": {"labels": {"username": "username"}, "name": "preferred"}
42-
}
52+
sample_server_manifest_preferred = {"metadata": {"labels": {"username": "username"}, "name": "preferred"}}
4353
mock_server_cache.list_servers.return_value = [sample_server_manifest_preferred]
4454
renku_ns_client.list_servers.return_value = []
4555
sessions_ns_client.list_servers.return_value = [sample_server_manifest]
@@ -86,9 +96,7 @@ def test_get_two_results_raises_error(mock_server_cache, mock_namespaced_client)
8696
def test_get_cache_is_preferred(mock_server_cache, mock_namespaced_client):
8797
renku_ns_client = mock_namespaced_client("renku")
8898
sessions_ns_client = mock_namespaced_client("renku-sessions")
89-
sample_server_manifest_cache = {
90-
"metadata": {"labels": {"username": "username"}, "name": "server"}
91-
}
99+
sample_server_manifest_cache = {"metadata": {"labels": {"username": "username"}, "name": "server"}}
92100
sample_server_manifest_non_cache = {
93101
"metadata": {
94102
"labels": {"username": "username", "not_preferred": True},
@@ -112,3 +120,82 @@ def test_get_server_no_match(mock_server_cache, mock_namespaced_client):
112120
client = K8sClient(mock_server_cache, renku_ns_client, "username", sessions_ns_client)
113121
server = client.get_server("server", "username")
114122
assert server is None
123+
124+
125+
def test_find_env_var():
126+
container = V1Container(
127+
name="test", env=[V1EnvVar(name="key1", value="val1"), V1EnvVar(name="key2", value_from=V1EnvVarSource())]
128+
)
129+
assert find_env_var(container, "key1") == (0, "val1")
130+
assert find_env_var(container, "key2") == (1, V1EnvVarSource())
131+
assert find_env_var(container, "missing") is None
132+
133+
134+
def test_patch_statefulset_tokens():
135+
git_clone_access_env = "GIT_CLONE_USER__RENKU_TOKEN"
136+
git_proxy_access_env = "GIT_PROXY_RENKU_ACCESS_TOKEN"
137+
git_proxy_refresh_env = "GIT_PROXY_RENKU_REFRESH_TOKEN"
138+
secrets_access_env = "RENKU_ACCESS_TOKEN"
139+
git_clone = V1Container(
140+
name="git-clone",
141+
env=[
142+
V1EnvVar(name="test", value="value"),
143+
V1EnvVar(git_clone_access_env, "old_value"),
144+
V1EnvVar(name="test-from-source", value_from=V1EnvVarSource()),
145+
],
146+
)
147+
git_proxy = V1Container(
148+
name="git-proxy",
149+
env=[
150+
V1EnvVar(name="test", value="value"),
151+
V1EnvVar(name="test-from-source", value_from=V1EnvVarSource()),
152+
V1EnvVar(git_proxy_refresh_env, "old_value"),
153+
V1EnvVar(git_proxy_access_env, "old_value"),
154+
],
155+
)
156+
secrets = V1Container(
157+
name="init-user-secrets",
158+
env=[
159+
V1EnvVar(secrets_access_env, "old_value"),
160+
V1EnvVar(name="test", value="value"),
161+
V1EnvVar(name="test-from-source", value_from=V1EnvVarSource()),
162+
],
163+
)
164+
random1 = V1Container(name="random1")
165+
random2 = V1Container(
166+
name="random2",
167+
env=[
168+
V1EnvVar(name="test", value="value"),
169+
V1EnvVar(name="test-from-source", value_from=V1EnvVarSource()),
170+
],
171+
)
172+
173+
new_renku_tokens = RenkuTokens(access_token="new_renku_access_token", refresh_token="new_renku_refresh_token")
174+
175+
sts = V1StatefulSet(
176+
spec=V1StatefulSetSpec(
177+
service_name="test",
178+
selector=V1LabelSelector(),
179+
template=V1PodTemplateSpec(
180+
spec=V1PodSpec(
181+
containers=[git_proxy, random1, random2], init_containers=[git_clone, random1, secrets, random2]
182+
)
183+
),
184+
)
185+
)
186+
patches = NamespacedK8sClient._get_statefulset_token_patches(sts, new_renku_tokens)
187+
188+
# Order of patches should be git proxy access, git proxy refresh, git clone, secrets
189+
assert len(patches) == 4
190+
# Git proxy access token
191+
assert patches[0]["path"] == "/spec/template/spec/containers/0/env/3/value"
192+
assert patches[0]["value"] == new_renku_tokens.access_token
193+
# Git proxy refresh token
194+
assert patches[1]["path"] == "/spec/template/spec/containers/0/env/2/value"
195+
assert patches[1]["value"] == new_renku_tokens.refresh_token
196+
# Git clone
197+
assert patches[2]["path"] == "/spec/template/spec/initContainers/0/env/1/value"
198+
assert patches[2]["value"] == new_renku_tokens.access_token
199+
# Secrets init
200+
assert patches[3]["path"] == "/spec/template/spec/initContainers/2/env/0/value"
201+
assert patches[3]["value"] == new_renku_tokens.access_token

0 commit comments

Comments
 (0)