Skip to content

Commit 63bd3d5

Browse files
Add e2e tests for SharedInformer against a real cluster
Co-authored-by: brendandburns <5751682+brendandburns@users.noreply.github.com>
1 parent d21aa1b commit 63bd3d5

1 file changed

Lines changed: 177 additions & 0 deletions

File tree

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
# Copyright 2024 The Kubernetes Authors.
2+
# Licensed under the Apache License, Version 2.0 (the "License").
3+
# End-to-end tests for kubernetes.informer.SharedInformer.
4+
5+
import threading
6+
import time
7+
import unittest
8+
import uuid
9+
10+
from kubernetes.client import api_client
11+
from kubernetes.client.api import core_v1_api
12+
from kubernetes.e2e_test import base
13+
from kubernetes.informer import ADDED, DELETED, MODIFIED, SharedInformer
14+
15+
_TIMEOUT = 30
16+
17+
18+
def _uid():
19+
return str(uuid.uuid4())[-12:]
20+
21+
22+
def _cm(name, payload=None):
23+
return {
24+
"apiVersion": "v1",
25+
"kind": "ConfigMap",
26+
"metadata": {"name": name, "labels": {"inf-e2e": "1"}},
27+
"data": payload or {"k": "v"},
28+
}
29+
30+
31+
def _name_of(obj):
32+
if hasattr(obj, "metadata"):
33+
return obj.metadata.name
34+
return (obj.get("metadata") or {}).get("name")
35+
36+
37+
class TestSharedInformerE2E(unittest.TestCase):
38+
39+
@classmethod
40+
def setUpClass(cls):
41+
cls.cfg = base.get_e2e_configuration()
42+
cls.apiclient = api_client.ApiClient(configuration=cls.cfg)
43+
cls.api = core_v1_api.CoreV1Api(cls.apiclient)
44+
45+
def _drop(self, cm_name):
46+
try:
47+
self.api.delete_namespaced_config_map(name=cm_name, namespace="default")
48+
except Exception:
49+
pass
50+
51+
def _expect(self, ev, label):
52+
if not ev.wait(timeout=_TIMEOUT):
53+
self.fail("Timeout waiting for: " + label)
54+
55+
def _wait_in_cache(self, inf, key):
56+
stop = time.monotonic() + _TIMEOUT
57+
while time.monotonic() < stop:
58+
if inf.cache.get_by_key(key) is not None:
59+
return
60+
time.sleep(0.25)
61+
self.fail("key " + key + " never appeared in cache")
62+
63+
def _wait_listed(self, inf):
64+
stop = time.monotonic() + _TIMEOUT
65+
while inf._resource_version is None and time.monotonic() < stop:
66+
time.sleep(0.1)
67+
self.assertIsNotNone(inf._resource_version, "initial list never completed")
68+
69+
# -------------------------------------------------------
70+
71+
def test_cache_populated_after_start(self):
72+
"""Pre-existing ConfigMaps appear in the cache once the informer starts."""
73+
name = "inf-pre-" + _uid()
74+
self.api.create_namespaced_config_map(body=_cm(name), namespace="default")
75+
self.addCleanup(self._drop, name)
76+
77+
inf = SharedInformer(
78+
list_func=self.api.list_namespaced_config_map,
79+
namespace="default",
80+
label_selector="inf-e2e=1",
81+
)
82+
inf.start()
83+
self.addCleanup(inf.stop)
84+
85+
self._wait_in_cache(inf, "default/" + name)
86+
self.assertEqual(_name_of(inf.cache.get_by_key("default/" + name)), name)
87+
88+
def test_added_event_and_cache_entry(self):
89+
"""Creating a ConfigMap fires ADDED and the object appears in the cache."""
90+
name = "inf-add-" + _uid()
91+
seen = threading.Event()
92+
93+
inf = SharedInformer(
94+
list_func=self.api.list_namespaced_config_map,
95+
namespace="default",
96+
label_selector="inf-e2e=1",
97+
)
98+
inf.add_event_handler(ADDED, lambda o: seen.set() if _name_of(o) == name else None)
99+
inf.start()
100+
self.addCleanup(inf.stop)
101+
self.addCleanup(self._drop, name)
102+
103+
self._wait_listed(inf)
104+
self.api.create_namespaced_config_map(body=_cm(name), namespace="default")
105+
self._expect(seen, "ADDED/" + name)
106+
self.assertIsNotNone(inf.cache.get_by_key("default/" + name))
107+
108+
def test_modified_event_and_cache_refresh(self):
109+
"""Patching a ConfigMap fires MODIFIED and the cache holds the updated object."""
110+
name = "inf-mod-" + _uid()
111+
seen = threading.Event()
112+
113+
inf = SharedInformer(
114+
list_func=self.api.list_namespaced_config_map,
115+
namespace="default",
116+
label_selector="inf-e2e=1",
117+
)
118+
inf.add_event_handler(MODIFIED, lambda o: seen.set() if _name_of(o) == name else None)
119+
inf.start()
120+
self.addCleanup(inf.stop)
121+
self.addCleanup(self._drop, name)
122+
123+
self.api.create_namespaced_config_map(body=_cm(name), namespace="default")
124+
self._wait_in_cache(inf, "default/" + name)
125+
126+
self.api.patch_namespaced_config_map(
127+
name=name, namespace="default", body={"data": {"k": "updated"}}
128+
)
129+
self._expect(seen, "MODIFIED/" + name)
130+
self.assertIsNotNone(inf.cache.get_by_key("default/" + name))
131+
132+
def test_deleted_event_removes_from_cache(self):
133+
"""Deleting a ConfigMap fires DELETED and removes it from the cache."""
134+
name = "inf-del-" + _uid()
135+
seen = threading.Event()
136+
137+
inf = SharedInformer(
138+
list_func=self.api.list_namespaced_config_map,
139+
namespace="default",
140+
label_selector="inf-e2e=1",
141+
)
142+
inf.add_event_handler(DELETED, lambda o: seen.set() if _name_of(o) == name else None)
143+
inf.start()
144+
self.addCleanup(inf.stop)
145+
146+
self.api.create_namespaced_config_map(body=_cm(name), namespace="default")
147+
self._wait_in_cache(inf, "default/" + name)
148+
149+
self.api.delete_namespaced_config_map(name=name, namespace="default")
150+
self._expect(seen, "DELETED/" + name)
151+
self.assertIsNone(inf.cache.get_by_key("default/" + name))
152+
153+
def test_resource_version_advances(self):
154+
"""The stored resourceVersion advances after watch events are received."""
155+
name = "inf-rv-" + _uid()
156+
seen = threading.Event()
157+
158+
inf = SharedInformer(
159+
list_func=self.api.list_namespaced_config_map,
160+
namespace="default",
161+
label_selector="inf-e2e=1",
162+
)
163+
inf.add_event_handler(ADDED, lambda o: seen.set() if _name_of(o) == name else None)
164+
inf.start()
165+
self.addCleanup(inf.stop)
166+
self.addCleanup(self._drop, name)
167+
168+
self._wait_listed(inf)
169+
rv_before = int(inf._resource_version)
170+
171+
self.api.create_namespaced_config_map(body=_cm(name), namespace="default")
172+
self._expect(seen, "ADDED/" + name)
173+
self.assertGreater(int(inf._resource_version), rv_before)
174+
175+
176+
if __name__ == "__main__":
177+
unittest.main()

0 commit comments

Comments
 (0)