Handle empty 'helm list' result when there is nothing deployed

The existing code assumes that there are always applications deployed
and the result is never an empty list.
The previous implementation ignored the return code when the subprocess
was killed by the timeout handler.
Split the method in two submethods for helm v2 and v3 implementations.

Closes-Bug: 1923587
Signed-off-by: Andrei Grosu <andrei.grosu@windriver.com>
Signed-off-by: Angie Wang <angie.wang@windriver.com>
Change-Id: Ib547bdb20c39e35c1538e3abb90108f7e3cad228
This commit is contained in:
Andrei Grosu 2021-05-05 13:03:50 +00:00 committed by Angie Wang
parent 37d348ae6d
commit 12fff41d78
1 changed files with 76 additions and 47 deletions

View File

@ -1,6 +1,6 @@
# sim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright (c) 2019 Wind River Systems, Inc.
# Copyright (c) 2019-2021 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
@ -25,6 +25,16 @@ import retrying
LOG = logging.getLogger(__name__)
# TODO(agrosu):
# There is a lot of duplicate code just to execute a helm command
# in a subshel.
# We should either move to a Helm API or, at least, move all this
# suprocess calling and error handling into a common function/object.
# python3 supports a 'timeout' parameter for +communicate() which
# will raise a subprocess.TimeoutExpired.
# When python3 migration is finished, the explicit timer should
# be removed.
def kill_process_and_descendants(proc):
# function to kill a process and its children processes
for child in psutil.Process(proc.pid).children(recursive=True):
@ -47,16 +57,7 @@ def refresh_helm_repo_information():
rpcapi.refresh_helm_repo_information(context.get_admin_context())
def retrieve_helm_releases():
"""Retrieve the deployed helm releases from tiller
Get the name, namespace and version for the deployed releases
by querying helm tiller
:return: a dict of deployed helm releases
"""
deployed_releases = {}
# Helm v3 releases
def retrieve_helm_v3_releases():
helm_list = subprocess.Popen(
['helm', '--kubeconfig', kubernetes.KUBERNETES_ADMIN_CONF,
'list', '--all-namespaces', '--output', 'yaml'],
@ -64,34 +65,42 @@ def retrieve_helm_releases():
timer = threading.Timer(20, kill_process_and_descendants, [helm_list])
try:
releases = {}
timer.start()
out, err = helm_list.communicate()
if out and not err:
releases = yaml.safe_load(out)
elif err and not out:
if helm_list.returncode != 0:
if err:
raise exception.HelmTillerFailure(reason=err)
# killing the subprocesses with +kill() when timer expires returns EBADF
# because the pipe is closed, but no error string on stderr.
if helm_list.returncode == -9:
raise exception.HelmTillerFailure(
reason="helm list operation timed out after "
"20 seconds. Terminated by threading timer.")
raise exception.HelmTillerFailure(
reason="Failed to retrieve releases: %s" % err)
elif not err and not out:
err_msg = "Failed to retrieve releases. " \
"Helm tiller response timeout."
raise exception.HelmTillerFailure(reason=err_msg)
reason="helm list operation failed without error "
"message, errno=%s" % helm_list.returncode)
for r in releases:
r_name = r.get('name')
r_version = r.get('revision')
r_namespace = r.get('namespace')
deployed_releases = {}
if out:
releases = yaml.safe_load(out)
for r in releases:
r_name = r.get('name')
r_version = r.get('revision')
r_namespace = r.get('namespace')
deployed_releases.setdefault(r_name, {}).update(
{r_namespace: r_version})
deployed_releases.setdefault(r_name, {}).update(
{r_namespace: r_version})
return deployed_releases
except Exception as e:
raise exception.HelmTillerFailure(
reason="Failed to retrieve releases: %s" % e)
reason="Failed to retrieve helmv3 releases: %s" % e)
finally:
timer.cancel()
# Helm v2 releases
def retrieve_helm_v2_releases():
env = os.environ.copy()
env['PATH'] = '/usr/local/sbin:' + env['PATH']
env['KUBECONFIG'] = kubernetes.KUBERNETES_ADMIN_CONF
@ -103,34 +112,54 @@ def retrieve_helm_releases():
timer = threading.Timer(20, kill_process_and_descendants, [helm_list])
try:
releases = {}
timer.start()
out, err = helm_list.communicate()
if out and not err:
output = yaml.safe_load(out)
releases = output.get('Releases', None)
elif err and not out:
if helm_list.returncode != 0:
if err:
raise exception.HelmTillerFailure(reason=err)
# killing the subprocesses with +kill() when timer expires returns EBADF
# because the pipe is closed, but no error string on stderr.
if helm_list.returncode == -9:
raise exception.HelmTillerFailure(
reason="helmv2-cli -- helm list operation timed out after "
"20 seconds. Terminated by threading timer.")
raise exception.HelmTillerFailure(
reason="Failed to retrieve releases: %s" % err)
elif not err and not out:
err_msg = "Failed to retrieve releases. " \
"Helm tiller response timeout."
raise exception.HelmTillerFailure(reason=err_msg)
reason="helmv2-cli -- helm list operation failed without "
"error message, errno=%s" % helm_list.returncode)
for r in releases:
r_name = r.get('Name')
r_version = r.get('Revision')
r_namespace = r.get('Namespace')
deployed_releases = {}
if out:
output = yaml.safe_load(out)
releases = output.get('Releases', {})
for r in releases:
r_name = r.get('Name')
r_version = r.get('Revision')
r_namespace = r.get('Namespace')
deployed_releases.setdefault(r_name, {}).update(
{r_namespace: r_version})
deployed_releases.setdefault(r_name, {}).update(
{r_namespace: r_version})
return deployed_releases
except Exception as e:
raise exception.HelmTillerFailure(
reason="Failed to retrieve releases: %s" % e)
reason="Failed to retrieve helmv2 releases: %s" % e)
finally:
timer.cancel()
def retrieve_helm_releases():
"""Retrieve the deployed helm releases from tiller
Get the name, namespace and version for the deployed releases
by querying helm tiller
:return: a dict of deployed helm releases
"""
deployed_releases = {}
deployed_releases.update(retrieve_helm_v3_releases())
deployed_releases.update(retrieve_helm_v2_releases())
return deployed_releases