Skip to content

Commit b6756dc

Browse files
committed
Cover Kubernetes utility edge cases
Keep generated task source and YAML deletion behavior protected against regressions identified in review.
1 parent 815033f commit b6756dc

2 files changed

Lines changed: 98 additions & 39 deletions

File tree

‎providers/cncf/kubernetes/tests/unit/cncf/kubernetes/test_python_kubernetes_script.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@
4343
"@setup\n@teardown\n@task.kubernetes()\ndef callable():\n return 1\n",
4444
"def callable():\n return 1\n",
4545
),
46+
(
47+
"@before\n@setup\n@teardown(on_failure_fail_dagrun=True)\n"
48+
'@task.kubernetes(image=build_image(name="worker"))\n@after\ndef callable():\n return 1\n',
49+
"@before\n@after\ndef callable():\n return 1\n",
50+
),
4651
],
4752
)
4853
def test_remove_task_decorator_removes_task_and_lifecycle_decorators(python_source, expected):

‎providers/cncf/kubernetes/tests/unit/cncf/kubernetes/utils/test_delete_from.py‎

Lines changed: 93 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -24,58 +24,75 @@
2424
from airflow.providers.cncf.kubernetes.utils import delete_from
2525

2626

27-
def create_api_constructor(monkeypatch, api_class_name):
28-
api_constructor = mock.create_autospec(getattr(delete_from.client, api_class_name))
29-
monkeypatch.setattr(delete_from.client, api_class_name, api_constructor)
30-
return api_constructor
31-
32-
3327
@pytest.mark.parametrize(
34-
("api_version", "kind", "api_class_name", "method_name"),
28+
("api_version", "kind", "method_name", "expected_kwargs"),
3529
[
36-
("v1", "ConfigMap", "CoreV1Api", "delete_namespaced_config_map"),
37-
("apps/v1", "Deployment", "AppsV1Api", "delete_namespaced_deployment"),
3830
(
39-
"apiextensions.k8s.io/v1",
40-
"CustomResourceDefinition",
41-
"ApiextensionsV1Api",
42-
"delete_custom_resource_definition",
31+
"v1",
32+
"ConfigMap",
33+
"delete_namespaced_config_map",
34+
{"name": "resource", "namespace": "default", "body": delete_from.DEFAULT_DELETION_BODY},
4335
),
4436
(
45-
"flowcontrol.apiserver.k8s.io/v1",
46-
"FlowSchema",
47-
"FlowcontrolApiserverV1Api",
48-
"delete_flow_schema",
37+
"apps/v1",
38+
"Deployment",
39+
"delete_namespaced_deployment",
40+
{"name": "resource", "namespace": "default", "body": delete_from.DEFAULT_DELETION_BODY},
4941
),
5042
],
5143
)
52-
def test_delete_from_yaml_derives_api_class_and_kind(
53-
monkeypatch, api_version, kind, api_class_name, method_name
44+
@mock.patch("airflow.providers.cncf.kubernetes.utils.delete_from.client.AppsV1Api", autospec=True)
45+
@mock.patch("airflow.providers.cncf.kubernetes.utils.delete_from.client.CoreV1Api", autospec=True)
46+
def test_delete_from_yaml_derives_namespaced_api_class_and_kind(
47+
mock_core_v1_api, mock_apps_v1_api, api_version, kind, method_name, expected_kwargs
5448
):
55-
api_constructor = create_api_constructor(monkeypatch, api_class_name)
56-
api_client = mock.sentinel.api_client
49+
api_constructor = mock_core_v1_api if api_version == "v1" else mock_apps_v1_api
5750
api = api_constructor.return_value
5851
getattr(api, method_name).return_value = mock.Mock(status="Success")
5952

6053
delete_from._delete_from_yaml_single_item(
61-
k8s_client=api_client,
54+
k8s_client=mock.sentinel.api_client,
6255
yml_document={"apiVersion": api_version, "kind": kind, "metadata": {"name": "resource"}},
6356
)
6457

65-
api_constructor.assert_called_once_with(api_client)
66-
if method_name.startswith("delete_namespaced"):
67-
getattr(api, method_name).assert_called_once_with(
68-
name="resource", namespace="default", body=delete_from.DEFAULT_DELETION_BODY
69-
)
70-
else:
71-
getattr(api, method_name).assert_called_once_with(
72-
name="resource", body=delete_from.DEFAULT_DELETION_BODY
73-
)
58+
api_constructor.assert_called_once_with(mock.sentinel.api_client)
59+
getattr(api, method_name).assert_called_once_with(**expected_kwargs)
7460

7561

76-
def test_delete_from_yaml_document_namespace_takes_precedence(monkeypatch):
77-
api_constructor = create_api_constructor(monkeypatch, "CoreV1Api")
62+
@pytest.mark.parametrize(
63+
("api_version", "kind", "method_name"),
64+
[
65+
("apiextensions.k8s.io/v1", "CustomResourceDefinition", "delete_custom_resource_definition"),
66+
("flowcontrol.apiserver.k8s.io/v1", "FlowSchema", "delete_flow_schema"),
67+
],
68+
)
69+
@mock.patch(
70+
"airflow.providers.cncf.kubernetes.utils.delete_from.client.FlowcontrolApiserverV1Api", autospec=True
71+
)
72+
@mock.patch("airflow.providers.cncf.kubernetes.utils.delete_from.client.ApiextensionsV1Api", autospec=True)
73+
def test_delete_from_yaml_derives_cluster_scoped_api_class_and_kind(
74+
mock_apiextensions_v1_api, mock_flowcontrol_apiserver_v1_api, api_version, kind, method_name
75+
):
76+
api_constructor = (
77+
mock_apiextensions_v1_api
78+
if api_version == "apiextensions.k8s.io/v1"
79+
else mock_flowcontrol_apiserver_v1_api
80+
)
7881
api = api_constructor.return_value
82+
getattr(api, method_name).return_value = mock.Mock(status="Success")
83+
84+
delete_from._delete_from_yaml_single_item(
85+
k8s_client=mock.sentinel.api_client,
86+
yml_document={"apiVersion": api_version, "kind": kind, "metadata": {"name": "resource"}},
87+
)
88+
89+
api_constructor.assert_called_once_with(mock.sentinel.api_client)
90+
getattr(api, method_name).assert_called_once_with(name="resource", body=delete_from.DEFAULT_DELETION_BODY)
91+
92+
93+
@mock.patch("airflow.providers.cncf.kubernetes.utils.delete_from.client.CoreV1Api", autospec=True)
94+
def test_delete_from_yaml_document_namespace_takes_precedence(mock_core_v1_api):
95+
api = mock_core_v1_api.return_value
7996
api.delete_namespaced_config_map.return_value = mock.Mock(status="Success")
8097

8198
delete_from._delete_from_yaml_single_item(
@@ -93,9 +110,9 @@ def test_delete_from_yaml_document_namespace_takes_precedence(monkeypatch):
93110
)
94111

95112

96-
def test_delete_from_dict_expands_lists_and_inherits_api_version(monkeypatch):
97-
api_constructor = create_api_constructor(monkeypatch, "CoreV1Api")
98-
api = api_constructor.return_value
113+
@mock.patch("airflow.providers.cncf.kubernetes.utils.delete_from.client.CoreV1Api", autospec=True)
114+
def test_delete_from_dict_expands_lists_and_inherits_api_version(mock_core_v1_api):
115+
api = mock_core_v1_api.return_value
99116
api.delete_namespaced_config_map.return_value = mock.Mock(status="Success")
100117
document = {
101118
"apiVersion": "v1",
@@ -129,9 +146,9 @@ def test_delete_from_dict_expands_lists_and_inherits_api_version(monkeypatch):
129146
)
130147

131148

132-
def test_delete_from_dict_collects_api_exceptions(monkeypatch):
133-
api_constructor = create_api_constructor(monkeypatch, "CoreV1Api")
134-
api = api_constructor.return_value
149+
@mock.patch("airflow.providers.cncf.kubernetes.utils.delete_from.client.CoreV1Api", autospec=True)
150+
def test_delete_from_dict_collects_api_exceptions(mock_core_v1_api):
151+
api = mock_core_v1_api.return_value
135152
first_exception = ApiException(reason="first")
136153
first_exception.body = "first body"
137154
second_exception = ApiException(reason="second")
@@ -154,3 +171,40 @@ def test_delete_from_dict_collects_api_exceptions(monkeypatch):
154171
assert (
155172
str(error.value) == "Error from server (first):first body\nError from server (second):second body\n"
156173
)
174+
175+
176+
@mock.patch("airflow.providers.cncf.kubernetes.utils.delete_from.delete_from_dict", autospec=True)
177+
def test_delete_from_yaml_skips_empty_documents_and_forwards_verbose(mock_delete_from_dict):
178+
document = {"apiVersion": "v1", "kind": "ConfigMap", "metadata": {"name": "resource"}}
179+
180+
delete_from.delete_from_yaml(
181+
k8s_client=mock.sentinel.api_client,
182+
yaml_objects=[None, document],
183+
verbose=True,
184+
namespace="configured-namespace",
185+
body=mock.sentinel.body,
186+
grace_period_seconds=0,
187+
)
188+
189+
mock_delete_from_dict.assert_called_once_with(
190+
k8s_client=mock.sentinel.api_client,
191+
data=document,
192+
body=mock.sentinel.body,
193+
namespace="configured-namespace",
194+
verbose=True,
195+
grace_period_seconds=0,
196+
)
197+
198+
199+
@mock.patch("airflow.providers.cncf.kubernetes.utils.delete_from.client.CoreV1Api", autospec=True)
200+
def test_delete_from_yaml_prints_status_when_verbose(mock_core_v1_api, capsys):
201+
api = mock_core_v1_api.return_value
202+
api.delete_namespaced_config_map.return_value = mock.Mock(status="Success")
203+
204+
delete_from._delete_from_yaml_single_item(
205+
k8s_client=mock.sentinel.api_client,
206+
yml_document={"apiVersion": "v1", "kind": "ConfigMap", "metadata": {"name": "resource"}},
207+
verbose=True,
208+
)
209+
210+
assert capsys.readouterr().out == "config_map deleted. status='Success'\n"

0 commit comments

Comments
 (0)