Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

salt: Add retry on DynamicClient creation to avoid flaky #3979

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# CHANGELOG
## Release 124.1.4 (in development)

### Bug fixes

- Fix flaky invalid `HTTPSConnectionPool` exception raised when loading
the pillar
(PR[#3979](https://github.com/scality/metalk8s/pull/3979))

## Release 124.1.3

### Enhancements
Expand Down
1 change: 1 addition & 0 deletions buildchain/buildchain/salt_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,7 @@ def task(self) -> types.TaskDict:
Path("salt/_states/metalk8s_package_manager.py"),
Path("salt/_states/metalk8s_sysctl.py"),
Path("salt/_states/metalk8s_volumes.py"),
Path("salt/_utils/metalk8s_kubernetes.py"),
Path("salt/_utils/metalk8s_utils.py"),
Path("salt/_utils/pillar_utils.py"),
Path("salt/_utils/volume_utils.py"),
Expand Down
4 changes: 1 addition & 3 deletions salt/_modules/metalk8s_drain.py
Original file line number Diff line number Diff line change
Expand Up @@ -522,9 +522,7 @@ def evict_pod(name, namespace="default", grace_period=1, **kwargs):

kubeconfig, context = __salt__["metalk8s_kubernetes.get_kubeconfig"](**kwargs)

client = kubernetes.dynamic.DynamicClient(
kubernetes.config.new_client_from_config(kubeconfig, context)
)
client = __utils__["metalk8s_kubernetes.get_client"](kubeconfig, context)

# DynamicClient does not handle Pod eviction, so compute the path manually
path = (
Expand Down
9 changes: 2 additions & 7 deletions salt/_modules/metalk8s_kubernetes.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@

try:
import kubernetes.client as k8s_client
import kubernetes
from kubernetes.dynamic.exceptions import ResourceNotFoundError
from kubernetes.client.rest import ApiException
except ImportError:
Expand Down Expand Up @@ -149,9 +148,7 @@ def method(

kubeconfig, context = __salt__["metalk8s_kubernetes.get_kubeconfig"](**kwargs)

client = kubernetes.dynamic.DynamicClient(
kubernetes.config.new_client_from_config(kubeconfig, context)
)
client = __utils__["metalk8s_kubernetes.get_client"](kubeconfig, context)

try:
api = client.resources.get(
Expand Down Expand Up @@ -379,9 +376,7 @@ def list_objects(
"""
kubeconfig, context = __salt__["metalk8s_kubernetes.get_kubeconfig"](**kwargs)

client = kubernetes.dynamic.DynamicClient(
kubernetes.config.new_client_from_config(kubeconfig, context)
)
client = __utils__["metalk8s_kubernetes.get_client"](kubeconfig, context)
try:
api = client.resources.get(api_version=apiVersion, kind=kind)
except ResourceNotFoundError as exc:
Expand Down
5 changes: 1 addition & 4 deletions salt/_modules/metalk8s_kubernetes_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

MISSING_DEPS = []
try:
import kubernetes
from kubernetes.client.rest import ApiException
except ImportError:
MISSING_DEPS.append("kubernetes")
Expand Down Expand Up @@ -85,9 +84,7 @@ def get_version_info(**kwargs):
kubeconfig, context = get_kubeconfig(**kwargs)

try:
client = kubernetes.dynamic.DynamicClient(
kubernetes.config.new_client_from_config(kubeconfig, context)
)
client = __utils__["metalk8s_kubernetes.get_client"](kubeconfig, context)
return client.version["kubernetes"]
except (ApiException, HTTPError) as exc:
raise CommandExecutionError("Failed to get version info") from exc
Expand Down
38 changes: 38 additions & 0 deletions salt/_utils/metalk8s_kubernetes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Utility methods for MetalK8s Kubernetes modules.
"""

import time

MISSING_DEPS = []

try:
import kubernetes
except ImportError:
MISSING_DEPS.append("kubernetes")

__virtualname__ = "metalk8s_kubernetes"


def __virtual__():
if MISSING_DEPS:
error_msg = "Missing dependencies: {}".format(", ".join(MISSING_DEPS))
return False, error_msg

return __virtualname__


def get_client(kubeconfig, context, attempts=5):
"""
Simple wrapper to retry on DynamicClient creation since it
may fail from time to time
"""
while True:
try:
return kubernetes.dynamic.DynamicClient(
kubernetes.config.new_client_from_config(kubeconfig, context)
)
except Exception: # pylint: disable=broad-except
if attempts < 0:
raise
attempts -= 1
time.sleep(5)
14 changes: 9 additions & 5 deletions salt/tests/unit/modules/test_metalk8s_drain.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,15 @@ def _create_mock(*args, **kwargs):
dynamic_client_mock = MagicMock()
dynamic_client_mock.request.side_effect = create_mock

dynamic_mock = MagicMock()
dynamic_mock.DynamicClient.return_value = dynamic_client_mock
with patch("kubernetes.dynamic", dynamic_mock), patch(
"kubernetes.config", MagicMock()
), capture_logs(metalk8s_drain.log, logging.DEBUG) as captured:
utils_dict = {
"metalk8s_kubernetes.get_client": MagicMock(
return_value=dynamic_client_mock
)
}

with patch.dict(metalk8s_drain.__utils__, utils_dict), capture_logs(
metalk8s_drain.log, logging.DEBUG
) as captured:
if raises:
self.assertRaisesRegex(
CommandExecutionError, result, metalk8s_drain.evict_pod, **kwargs
Expand Down
95 changes: 55 additions & 40 deletions salt/tests/unit/modules/test_metalk8s_kubernetes.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,9 @@ def _resources_get_mock(api_version, kind):
dynamic_client_mock = MagicMock()
dynamic_client_mock.resources.get.side_effect = _resources_get_mock

dynamic_mock = MagicMock()
dynamic_mock.DynamicClient.return_value = dynamic_client_mock
get_client_mock = MagicMock(return_value=dynamic_client_mock)

return dynamic_mock
return get_client_mock


class Metalk8sKubernetesTestCase(TestCase, mixins.LoaderModuleMockMixin):
Expand Down Expand Up @@ -94,7 +93,7 @@ def test_virtual_success(self):

@parameterized.expand(
[
"kubernetes",
("kubernetes.client", "kubernetes"),
("urllib3.exceptions", "urllib3"),
]
)
Expand Down Expand Up @@ -140,9 +139,12 @@ def _create_mock(body, **_):
return obj

create_mock = MagicMock(side_effect=_create_mock)
dynamic_mock = _mock_k8s_dynamic(
namespaced=namespaced, action="create", mock=create_mock
)

utils_dict = {
"metalk8s_kubernetes.get_client": _mock_k8s_dynamic(
namespaced=namespaced, action="create", mock=create_mock
)
}

manifest_read_mock = MagicMock()
# None = IOError
Expand All @@ -157,9 +159,9 @@ def _create_mock(body, **_):
salt_dict = {
"metalk8s_kubernetes.read_and_render_yaml_file": manifest_read_mock
}
with patch("kubernetes.dynamic", dynamic_mock), patch(
"kubernetes.config", MagicMock()
), patch.dict(metalk8s_kubernetes.__salt__, salt_dict):
with patch.dict(metalk8s_kubernetes.__utils__, utils_dict), patch.dict(
metalk8s_kubernetes.__salt__, salt_dict
):
if raises:
self.assertRaisesRegex(
Exception, result, metalk8s_kubernetes.create_object, **kwargs
Expand Down Expand Up @@ -200,9 +202,12 @@ def _delete_mock(name, **_):
return res

delete_mock = MagicMock(side_effect=_delete_mock)
dynamic_mock = _mock_k8s_dynamic(
namespaced=namespaced, action="delete", mock=delete_mock
)

utils_dict = {
"metalk8s_kubernetes.get_client": _mock_k8s_dynamic(
namespaced=namespaced, action="delete", mock=delete_mock
)
}

manifest_read_mock = MagicMock()
# None = IOError
Expand All @@ -217,9 +222,9 @@ def _delete_mock(name, **_):
salt_dict = {
"metalk8s_kubernetes.read_and_render_yaml_file": manifest_read_mock
}
with patch("kubernetes.dynamic", dynamic_mock), patch(
"kubernetes.config", MagicMock()
), patch.dict(metalk8s_kubernetes.__salt__, salt_dict):
with patch.dict(metalk8s_kubernetes.__utils__, utils_dict), patch.dict(
metalk8s_kubernetes.__salt__, salt_dict
):
if raises:
self.assertRaisesRegex(
Exception, result, metalk8s_kubernetes.delete_object, **kwargs
Expand Down Expand Up @@ -258,9 +263,12 @@ def _replace_mock(body, **_):
return obj

replace_mock = MagicMock(side_effect=_replace_mock)
dynamic_mock = _mock_k8s_dynamic(
namespaced=namespaced, action="replace", mock=replace_mock
)

utils_dict = {
"metalk8s_kubernetes.get_client": _mock_k8s_dynamic(
namespaced=namespaced, action="replace", mock=replace_mock
)
}

manifest_read_mock = MagicMock()
# None = IOError
Expand All @@ -275,9 +283,9 @@ def _replace_mock(body, **_):
salt_dict = {
"metalk8s_kubernetes.read_and_render_yaml_file": manifest_read_mock
}
with patch("kubernetes.dynamic", dynamic_mock), patch(
"kubernetes.config", MagicMock()
), patch.dict(metalk8s_kubernetes.__salt__, salt_dict):
with patch.dict(metalk8s_kubernetes.__utils__, utils_dict), patch.dict(
metalk8s_kubernetes.__salt__, salt_dict
):
if raises:
self.assertRaisesRegex(
Exception, result, metalk8s_kubernetes.replace_object, **kwargs
Expand Down Expand Up @@ -320,9 +328,12 @@ def _get_mock(name, **_):
return res

get_mock = MagicMock(side_effect=_get_mock)
dynamic_mock = _mock_k8s_dynamic(
namespaced=namespaced, action="get", mock=get_mock
)

utils_dict = {
"metalk8s_kubernetes.get_client": _mock_k8s_dynamic(
namespaced=namespaced, action="get", mock=get_mock
)
}

manifest_read_mock = MagicMock()
# None = IOError
Expand All @@ -337,9 +348,9 @@ def _get_mock(name, **_):
salt_dict = {
"metalk8s_kubernetes.read_and_render_yaml_file": manifest_read_mock
}
with patch("kubernetes.dynamic", dynamic_mock), patch(
"kubernetes.config", MagicMock()
), patch.dict(metalk8s_kubernetes.__salt__, salt_dict):
with patch.dict(metalk8s_kubernetes.__utils__, utils_dict), patch.dict(
metalk8s_kubernetes.__salt__, salt_dict
):
if raises:
self.assertRaisesRegex(
Exception, result, metalk8s_kubernetes.get_object, **kwargs
Expand Down Expand Up @@ -377,9 +388,12 @@ def _patch_mock(body, **_):
return res

patch_mock = MagicMock(side_effect=_patch_mock)
dynamic_mock = _mock_k8s_dynamic(
namespaced=namespaced, action="patch", mock=patch_mock
)

utils_dict = {
"metalk8s_kubernetes.get_client": _mock_k8s_dynamic(
namespaced=namespaced, action="patch", mock=patch_mock
)
}

manifest_read_mock = MagicMock()
# None = IOError
Expand All @@ -394,9 +408,9 @@ def _patch_mock(body, **_):
salt_dict = {
"metalk8s_kubernetes.read_and_render_yaml_file": manifest_read_mock
}
with patch("kubernetes.dynamic", dynamic_mock), patch(
"kubernetes.config", MagicMock()
), patch.dict(metalk8s_kubernetes.__salt__, salt_dict):
with patch.dict(metalk8s_kubernetes.__utils__, utils_dict), patch.dict(
metalk8s_kubernetes.__salt__, salt_dict
):
if raises:
self.assertRaisesRegex(
Exception, result, metalk8s_kubernetes.update_object, **kwargs
Expand Down Expand Up @@ -473,13 +487,14 @@ def _list_mock(**_):
return res

list_mock = MagicMock(side_effect=_list_mock)
dynamic_mock = _mock_k8s_dynamic(
namespaced=namespaced, action="get", mock=list_mock
)

with patch("kubernetes.dynamic", dynamic_mock), patch(
"kubernetes.config", MagicMock()
):
utils_dict = {
"metalk8s_kubernetes.get_client": _mock_k8s_dynamic(
namespaced=namespaced, action="get", mock=list_mock
)
}

with patch.dict(metalk8s_kubernetes.__utils__, utils_dict):
if raises:
self.assertRaisesRegex(
Exception, result, metalk8s_kubernetes.list_objects, **kwargs
Expand Down
14 changes: 7 additions & 7 deletions salt/tests/unit/modules/test_metalk8s_kubernetes_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def test_virtual_success(self):

@parameterized.expand(
[
("kubernetes"),
(("kubernetes.client.rest", "kubernetes")),
(("urllib3.exceptions", "urllib3")),
]
)
Expand Down Expand Up @@ -109,16 +109,16 @@ def test_get_version_info(
dynamic_client_mock = MagicMock()
dynamic_client_mock.version = {"kubernetes": result}

dynamic_mock = MagicMock()
dynamic_mock.DynamicClient.return_value = dynamic_client_mock
get_client_mock = MagicMock(return_value=dynamic_client_mock)

utils_dict = {"metalk8s_kubernetes.get_client": get_client_mock}

if k8s_connection_raise:
dynamic_mock.DynamicClient.side_effect = HTTPError("Failed to connect")
get_client_mock.side_effect = HTTPError("Failed to connect")

with patch.object(
metalk8s_kubernetes_utils, "get_kubeconfig", kubeconfig_mock
), patch("kubernetes.dynamic", dynamic_mock), patch(
"kubernetes.config", MagicMock()
):
), patch.dict(metalk8s_kubernetes_utils.__utils__, utils_dict):
if raises:
self.assertRaisesRegex(
CommandExecutionError,
Expand Down