Skip to content

Commit 28c848b

Browse files
committed
Add operator test cases
Update CI to run tests
1 parent a3f0fd3 commit 28c848b

File tree

5 files changed

+405
-0
lines changed

5 files changed

+405
-0
lines changed

.github/workflows/ci.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,50 @@ jobs:
3131
- id: outputStep
3232
run: echo "changeDirs=${{ steps.changeDirsStep.outputs.all_changed_files }}" >> $GITHUB_OUTPUT
3333

34+
35+
server-tests:
36+
runs-on: ubuntu-latest
37+
steps:
38+
- name: Set up Python
39+
uses: actions/setup-python@v3
40+
with:
41+
python-version: '3.11'
42+
43+
- name: Install dependencies
44+
run: |
45+
pip install -r testing.requirements.txt
46+
pip install -r srv/requirements.txt
47+
48+
- name: Run tests
49+
run: pytest tests/server_tests
50+
51+
operator-tests:
52+
runs-on: ubuntu-latest
53+
steps:
54+
- name: Set up Python
55+
uses: actions/setup-python@v3
56+
with:
57+
python-version: '3.11'
58+
59+
- name: Setup k3s test cluster
60+
uses: nolar/setup-k3d-k3s@v1
61+
with:
62+
github-token: ${{ secrets.GITHUB_TOKEN }}
63+
64+
- name: Install dependencies
65+
run: |
66+
pip install -r testing.requirements.txt
67+
pip install -r opr/requirements.txt
68+
69+
- name: Run tests
70+
run: pytest tests/operator_tests
71+
3472
docker:
3573
runs-on: ubuntu-latest
3674
needs:
3775
- changed-dirs
76+
- server-tests
77+
- operator-tests
3878
if: contains(needs.changed-dirs.outputs.changeDirs, 'opr') || contains(needs.changed-dirs.outputs.changeDirs, 'srv')
3979
strategy:
4080
matrix:

tests/operator_tests/__init__.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import os
2+
import uuid
3+
import pytest
4+
import kubernetes
5+
from kubernetes.client import ApiClient, CoreV1Api, V1Namespace, V1ObjectMeta, AppsV1Api, CustomObjectsApi, ApiextensionsV1Api
6+
7+
8+
def delete_all(namespace, list_func, delete_func, patch_func):
9+
resources = list_func(namespace=namespace)
10+
try:
11+
resources_list = resources["items"]
12+
except TypeError:
13+
resources_list = resources.items
14+
for resource in resources_list:
15+
# Operator is not running in fixtures, so we need a force-delete (or this patch).
16+
patch_body = {
17+
"metadata": {
18+
"finalizers": []
19+
}
20+
}
21+
22+
try:
23+
name = resource.metadata.name
24+
except AttributeError:
25+
name = resource["metadata"]["name"]
26+
27+
try:
28+
patch_func(name=name, namespace=namespace, body=patch_body)
29+
except kubernetes.client.exceptions.ApiException as e:
30+
if e.status == 404:
31+
pass
32+
33+
try:
34+
delete_func(name=name, namespace=namespace)
35+
except kubernetes.client.exceptions.ApiException as e:
36+
if e.status == 404:
37+
pass
38+
39+
40+
def delete_all_custom_objects(crd_api, namespace, plural):
41+
def list_cr(namespace):
42+
return crd_api.list_namespaced_custom_object("datalab.tuwien.ac.at", "v1", namespace, plural)
43+
44+
def delete_cr(name, namespace):
45+
return crd_api.delete_namespaced_custom_object("datalab.tuwien.ac.at", "v1", namespace, plural, name=name)
46+
47+
def patch_cr(name, namespace, body):
48+
return crd_api.patch_namespaced_custom_object("datalab.tuwien.ac.at", "v1", namespace, plural, name=name, body=body)
49+
50+
delete_all(namespace, list_cr, delete_cr, patch_cr)
51+
52+
53+
@pytest.fixture(scope="function")
54+
def random_namespace():
55+
client = ApiClient(configuration=kubernetes.config.load_kube_config())
56+
api = CoreV1Api(api_client=client)
57+
58+
namespace = f'test-namespace-{uuid.uuid4().hex[:10]}'
59+
try:
60+
body = V1Namespace(metadata=V1ObjectMeta(name=namespace))
61+
api.create_namespace(body=body)
62+
yield namespace
63+
finally:
64+
apps_api = AppsV1Api(api_client=client)
65+
crd_api = CustomObjectsApi(api_client=client)
66+
extensions_api = ApiextensionsV1Api(api_client=client)
67+
68+
delete_all(namespace, api.list_namespaced_pod, api.delete_namespaced_pod, api.patch_namespaced_pod)
69+
delete_all(namespace, api.list_namespaced_config_map, api.delete_namespaced_config_map, api.patch_namespaced_config_map)
70+
delete_all(namespace, api.list_namespaced_service, api.delete_namespaced_service, api.patch_namespaced_service)
71+
delete_all(namespace, apps_api.list_namespaced_deployment, apps_api.delete_namespaced_deployment, apps_api.patch_namespaced_deployment)
72+
73+
delete_all_custom_objects(crd_api, namespace, "keyvaluepairs")
74+
delete_all_custom_objects(crd_api, namespace, "configservers")
75+
76+
config_server_crds = ["configservers.datalab.tuwien.ac.at", "keyvaluepairs.datalab.tuwien.ac.at"]
77+
for name in config_server_crds:
78+
extensions_api.delete_custom_resource_definition(name=name)
79+
80+
api.delete_namespace(name=namespace)
81+
client.close()
82+
83+
84+
@pytest.fixture(scope="session")
85+
def operator_file():
86+
return os.path.abspath(os.path.join(os.path.dirname(__file__), '../../opr/operator.py'))
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import kubernetes
2+
from kubernetes import watch
3+
from kubernetes.client import ApiClient, CustomObjectsApi, CoreV1Api, V1ConfigMap, V1Volume, V1Pod
4+
from kopf.testing import KopfRunner
5+
6+
from . import random_namespace, operator_file
7+
from .test_crd import create_crd
8+
9+
10+
def create_config_server(client: ApiClient, namespace: str):
11+
custom_objects_api = CustomObjectsApi(client)
12+
body = {
13+
"apiVersion": "datalab.tuwien.ac.at/v1",
14+
"kind": "ConfigServer",
15+
"metadata": {
16+
"name": "test-config-server",
17+
"namespace": namespace
18+
},
19+
"spec": {
20+
"image": "ghcr.io/tu-wien-datalab/config-server:main",
21+
"imagePullPolicy": "IfNotPresent",
22+
"containerPort": 80,
23+
"configMountPath": "/var/lib/config-server"
24+
}
25+
26+
}
27+
custom_objects_api.create_namespaced_custom_object("datalab.tuwien.ac.at", "v1", namespace, "configservers", body)
28+
29+
30+
def test_config_server_custom_resource(random_namespace, operator_file):
31+
client = ApiClient(configuration=kubernetes.config.load_kube_config())
32+
core_api = CoreV1Api(client)
33+
custom_objects_api = CustomObjectsApi(client)
34+
35+
create_crd(client)
36+
37+
with KopfRunner(['run', '-A', '--verbose', operator_file]) as runner:
38+
create_config_server(client, random_namespace)
39+
40+
config_server_watch = watch.Watch()
41+
entered = False
42+
for event in config_server_watch.stream(custom_objects_api.list_namespaced_custom_object, "datalab.tuwien.ac.at", "v1", random_namespace, "configservers"):
43+
assert event['type'] == "ADDED"
44+
obj = event['object'] # object is one of type return_type
45+
46+
entered = True
47+
assert obj["metadata"]["name"] == "test-config-server"
48+
config_server_watch.stop()
49+
assert entered
50+
51+
pod_watch = watch.Watch()
52+
entered = False
53+
54+
for event in pod_watch.stream(core_api.list_namespaced_pod, random_namespace, timeout_seconds=10):
55+
assert event['type'] == "ADDED"
56+
57+
obj = event['object'] # object is one of type return_type
58+
assert isinstance(obj, V1Pod)
59+
60+
entered = True
61+
62+
pod = obj
63+
assert pod.metadata.name.startswith("test-config-server")
64+
assert pod.status.phase in ["Running", "Pending"]
65+
volumes: list[V1Volume] = pod.spec.volumes
66+
assert any([v.config_map is not None for v in volumes])
67+
config_volumes = list(filter(lambda v: v.config_map is not None, volumes))
68+
assert len(config_volumes) == 1
69+
assert config_volumes[0].name == "config"
70+
config_map = config_volumes[0].config_map
71+
assert config_map.name == "test-config-server-values"
72+
73+
pod_watch.stop()
74+
assert entered
75+
76+
config_map_watch = watch.Watch()
77+
entered = False
78+
for event in config_map_watch.stream(core_api.list_namespaced_config_map, random_namespace, timeout_seconds=10):
79+
assert event['type'] == "ADDED"
80+
81+
obj = event['object'] # object is one of type return_type
82+
assert isinstance(obj, V1ConfigMap)
83+
84+
config_map: V1ConfigMap = obj
85+
if config_map.metadata.name != "test-config-server-values":
86+
continue
87+
88+
entered = True
89+
assert config_map.data is None
90+
91+
config_map_watch.stop()
92+
assert entered
93+
94+
assert runner.exit_code == 0
95+
assert runner.exception is None

tests/operator_tests/test_crd.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import os
2+
3+
import pytest
4+
import kubernetes
5+
from kubernetes.client import ApiClient, ApiextensionsV1Api, V1CustomResourceDefinition
6+
from kubernetes import utils
7+
8+
9+
def create_crd(client: ApiClient):
10+
yaml_file = os.path.join(os.path.dirname(__file__), '../../opr/crd.yaml')
11+
assert os.path.exists(yaml_file)
12+
utils.create_from_yaml(client, yaml_file)
13+
14+
15+
def test_create_crd():
16+
client = ApiClient(configuration=kubernetes.config.load_kube_config())
17+
extensions_api = ApiextensionsV1Api(api_client=client)
18+
19+
create_crd(client)
20+
21+
config_server_crds = ["configservers.datalab.tuwien.ac.at", "keyvaluepairs.datalab.tuwien.ac.at"]
22+
list_crds = lambda namespace: extensions_api.list_custom_resource_definition()
23+
try:
24+
crds: list[V1CustomResourceDefinition] = list_crds(None).items
25+
names = [c.metadata.name for c in crds]
26+
assert len(names) > 0
27+
assert set(names).issuperset(config_server_crds)
28+
finally:
29+
for name in config_server_crds:
30+
extensions_api.delete_custom_resource_definition(name=name)

0 commit comments

Comments
 (0)