Use helm 3 CLI as backend
Helm 3 breaking changes (likely non-exhaustive): - crd-install hook removed and replaced with crds directory in chart where all CRDs defined in it will be installed before any rendering of the chart - test-failure hook annotation value removed, and test-success deprecated. Use test instead - `--force` no longer handles recreating resources which cannot be updated due to e.g. immutability [0] - `--recreate-pods` removed, use declarative approach instead [1] [0]: https://github.com/helm/helm/issues/7082 [1]: https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments Signed-off-by: Sean Eagan <seaneagan1@gmail.com> Change-Id: I20ff40ba55197de3d37e5fd647e7d2524a53248f
This commit is contained in:
parent
4260e5335c
commit
68747d0815
|
@ -22,6 +22,7 @@ from oslo_log import log as logging
|
||||||
from oslo_utils import excutils
|
from oslo_utils import excutils
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
from armada.handlers.helm import Helm
|
||||||
from armada.handlers.tiller import Tiller
|
from armada.handlers.tiller import Tiller
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
|
@ -120,6 +121,9 @@ class BaseResource(object):
|
||||||
def get_tiller(self, req, resp):
|
def get_tiller(self, req, resp):
|
||||||
return Tiller()
|
return Tiller()
|
||||||
|
|
||||||
|
def get_helm(self, req, resp):
|
||||||
|
return Helm()
|
||||||
|
|
||||||
|
|
||||||
class ArmadaRequestContext(object):
|
class ArmadaRequestContext(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
|
@ -69,8 +69,8 @@ class Apply(api.BaseResource):
|
||||||
message="Request must be in application/x-yaml"
|
message="Request must be in application/x-yaml"
|
||||||
"or application/json")
|
"or application/json")
|
||||||
try:
|
try:
|
||||||
with self.get_tiller(req, resp) as tiller:
|
with self.get_helm(req, resp) as helm:
|
||||||
msg = self.handle(req, documents, tiller)
|
msg = self.handle(req, documents, helm)
|
||||||
resp.text = json.dumps({
|
resp.text = json.dumps({
|
||||||
'message': msg,
|
'message': msg,
|
||||||
})
|
})
|
||||||
|
@ -88,7 +88,7 @@ class Apply(api.BaseResource):
|
||||||
self.return_error(resp, falcon.HTTP_500, message=err_message)
|
self.return_error(resp, falcon.HTTP_500, message=err_message)
|
||||||
|
|
||||||
@lock_and_thread()
|
@lock_and_thread()
|
||||||
def handle(self, req, documents, tiller):
|
def handle(self, req, documents, helm):
|
||||||
armada = Armada(
|
armada = Armada(
|
||||||
documents,
|
documents,
|
||||||
disable_update_pre=req.get_param_as_bool('disable_update_pre'),
|
disable_update_pre=req.get_param_as_bool('disable_update_pre'),
|
||||||
|
@ -96,7 +96,7 @@ class Apply(api.BaseResource):
|
||||||
enable_chart_cleanup=req.get_param_as_bool('enable_chart_cleanup'),
|
enable_chart_cleanup=req.get_param_as_bool('enable_chart_cleanup'),
|
||||||
force_wait=req.get_param_as_bool('wait'),
|
force_wait=req.get_param_as_bool('wait'),
|
||||||
timeout=req.get_param_as_int('timeout'),
|
timeout=req.get_param_as_int('timeout'),
|
||||||
tiller=tiller,
|
helm=helm,
|
||||||
target_manifest=req.get_param('target_manifest'))
|
target_manifest=req.get_param('target_manifest'))
|
||||||
|
|
||||||
return armada.sync()
|
return armada.sync()
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
# Copyright 2017 The Armada Authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
import falcon
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log as logging
|
||||||
|
|
||||||
|
from armada import api
|
||||||
|
from armada.common import policy
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Releases(api.BaseResource):
|
||||||
|
@policy.enforce('armada:get_release')
|
||||||
|
def on_get(self, req, resp):
|
||||||
|
'''Controller for listing Helm releases.
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
with self.get_helm(req, resp) as helm:
|
||||||
|
releases = self.handle(helm)
|
||||||
|
resp.text = json.dumps({
|
||||||
|
'releases': releases,
|
||||||
|
})
|
||||||
|
resp.content_type = 'application/json'
|
||||||
|
resp.status = falcon.HTTP_200
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
err_message = 'Unable to find Helm Releases: {}'.format(e)
|
||||||
|
self.error(req.context, err_message)
|
||||||
|
self.return_error(resp, falcon.HTTP_500, message=err_message)
|
||||||
|
|
||||||
|
def handle(self, helm):
|
||||||
|
LOG.debug('Getting helm releases')
|
||||||
|
|
||||||
|
releases = {}
|
||||||
|
for release in helm.list_release_ids():
|
||||||
|
releases.setdefault(release.namespace, [])
|
||||||
|
releases[release.namespace].append(release.name)
|
||||||
|
return releases
|
|
@ -21,6 +21,7 @@ import yaml
|
||||||
from armada import api
|
from armada import api
|
||||||
from armada.common import policy
|
from armada.common import policy
|
||||||
from armada import const
|
from armada import const
|
||||||
|
from armada.handlers.helm import HelmReleaseId
|
||||||
from armada.handlers.lock import lock_and_thread, LockException
|
from armada.handlers.lock import lock_and_thread, LockException
|
||||||
from armada.handlers.manifest import Manifest
|
from armada.handlers.manifest import Manifest
|
||||||
from armada.handlers.test import Test
|
from armada.handlers.test import Test
|
||||||
|
@ -36,11 +37,12 @@ class TestReleasesReleaseNameController(api.BaseResource):
|
||||||
'''
|
'''
|
||||||
|
|
||||||
@policy.enforce('armada:test_release')
|
@policy.enforce('armada:test_release')
|
||||||
def on_get(self, req, resp, release):
|
def on_get(self, req, resp, namespace, release):
|
||||||
try:
|
try:
|
||||||
|
release_id = HelmReleaseId(namespace, release)
|
||||||
|
with self.get_helm(req, resp) as helm:
|
||||||
|
|
||||||
with self.get_tiller(req, resp) as tiller:
|
success = self.handle(req, release_id, helm)
|
||||||
success = self.handle(req, release, tiller)
|
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
msg = {
|
msg = {
|
||||||
|
@ -60,9 +62,8 @@ class TestReleasesReleaseNameController(api.BaseResource):
|
||||||
self.return_error(resp, falcon.HTTP_409, message=str(e))
|
self.return_error(resp, falcon.HTTP_409, message=str(e))
|
||||||
|
|
||||||
@lock_and_thread()
|
@lock_and_thread()
|
||||||
def handle(self, req, release, tiller):
|
def handle(self, req, release_id, helm):
|
||||||
cleanup = req.get_param_as_bool('cleanup')
|
test_handler = Test({}, release_id, helm)
|
||||||
test_handler = Test({}, release, tiller, cleanup=cleanup)
|
|
||||||
return test_handler.test_release_for_success()
|
return test_handler.test_release_for_success()
|
||||||
|
|
||||||
|
|
||||||
|
@ -111,13 +112,13 @@ class TestReleasesManifestController(api.BaseResource):
|
||||||
def on_post(self, req, resp):
|
def on_post(self, req, resp):
|
||||||
# TODO(fmontei): Validation Content-Type is application/x-yaml.
|
# TODO(fmontei): Validation Content-Type is application/x-yaml.
|
||||||
try:
|
try:
|
||||||
with self.get_tiller(req, resp) as tiller:
|
with self.get_helm(req, resp) as helm:
|
||||||
return self.handle(req, resp, tiller)
|
return self.handle(req, resp, helm)
|
||||||
except LockException as e:
|
except LockException as e:
|
||||||
self.return_error(resp, falcon.HTTP_409, message=str(e))
|
self.return_error(resp, falcon.HTTP_409, message=str(e))
|
||||||
|
|
||||||
@lock_and_thread()
|
@lock_and_thread()
|
||||||
def handle(self, req, resp, tiller):
|
def handle(self, req, resp, helm):
|
||||||
try:
|
try:
|
||||||
documents = self.req_yaml(req, default=[])
|
documents = self.req_yaml(req, default=[])
|
||||||
except yaml.YAMLError:
|
except yaml.YAMLError:
|
||||||
|
@ -134,7 +135,7 @@ class TestReleasesManifestController(api.BaseResource):
|
||||||
documents, target_manifest=target_manifest).get_manifest()
|
documents, target_manifest=target_manifest).get_manifest()
|
||||||
|
|
||||||
prefix = armada_obj[const.KEYWORD_DATA][const.KEYWORD_PREFIX]
|
prefix = armada_obj[const.KEYWORD_DATA][const.KEYWORD_PREFIX]
|
||||||
known_releases = [release[0] for release in tiller.list_charts()]
|
release_ids = helm.list_release_ids()
|
||||||
|
|
||||||
message = {'tests': {'passed': [], 'skipped': [], 'failed': []}}
|
message = {'tests': {'passed': [], 'skipped': [], 'failed': []}}
|
||||||
|
|
||||||
|
@ -143,31 +144,30 @@ class TestReleasesManifestController(api.BaseResource):
|
||||||
for ch in group.get(const.KEYWORD_CHARTS):
|
for ch in group.get(const.KEYWORD_CHARTS):
|
||||||
chart = ch['chart']
|
chart = ch['chart']
|
||||||
|
|
||||||
release_name = release_prefixer(prefix, chart.get('release'))
|
release_id = helm.HelmReleaseId(
|
||||||
if release_name in known_releases:
|
ch['namespace'], release_prefixer(prefix, ch['release']))
|
||||||
cleanup = req.get_param_as_bool('cleanup')
|
if release_id in release_ids:
|
||||||
enable_all = req.get_param_as_bool('enable_all')
|
enable_all = req.get_param_as_bool('enable_all')
|
||||||
cg_test_charts = group.get('test_charts')
|
cg_test_charts = group.get('test_charts')
|
||||||
|
|
||||||
test_handler = Test(
|
test_handler = Test(
|
||||||
chart,
|
chart,
|
||||||
release_name,
|
release_id,
|
||||||
tiller,
|
helm,
|
||||||
cg_test_charts=cg_test_charts,
|
cg_test_charts=cg_test_charts,
|
||||||
cleanup=cleanup,
|
|
||||||
enable_all=enable_all)
|
enable_all=enable_all)
|
||||||
|
|
||||||
if test_handler.test_enabled:
|
if test_handler.test_enabled:
|
||||||
success = test_handler.test_release_for_success()
|
success = test_handler.test_release_for_success()
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
message['test']['passed'].append(release_name)
|
message['test']['passed'].append(release_id)
|
||||||
else:
|
else:
|
||||||
message['test']['failed'].append(release_name)
|
message['test']['failed'].append(release_id)
|
||||||
else:
|
else:
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
'Release %s not found - SKIPPING', release_name)
|
'Release %s not found - SKIPPING', release_id)
|
||||||
message['test']['skipped'].append(release_name)
|
message['test']['skipped'].append(release_id)
|
||||||
|
|
||||||
resp.status = falcon.HTTP_200
|
resp.status = falcon.HTTP_200
|
||||||
resp.text = json.dumps(message)
|
resp.text = json.dumps(message)
|
||||||
|
|
|
@ -56,35 +56,3 @@ class Status(api.BaseResource):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return message
|
return message
|
||||||
|
|
||||||
|
|
||||||
class Release(api.BaseResource):
|
|
||||||
@policy.enforce('tiller:get_release')
|
|
||||||
def on_get(self, req, resp):
|
|
||||||
'''Controller for listing Tiller releases.
|
|
||||||
'''
|
|
||||||
try:
|
|
||||||
with self.get_tiller(req, resp) as tiller:
|
|
||||||
releases = self.handle(tiller)
|
|
||||||
resp.text = json.dumps({
|
|
||||||
'releases': releases,
|
|
||||||
})
|
|
||||||
resp.content_type = 'application/json'
|
|
||||||
resp.status = falcon.HTTP_200
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
err_message = 'Unable to find Tiller Releases: {}'.format(e)
|
|
||||||
self.error(req.context, err_message)
|
|
||||||
self.return_error(resp, falcon.HTTP_500, message=err_message)
|
|
||||||
|
|
||||||
def handle(self, tiller):
|
|
||||||
LOG.debug(
|
|
||||||
'Tiller (Release) at: %s:%s, namespace=%s, '
|
|
||||||
'timeout=%s', tiller.tiller_host, tiller.tiller_port,
|
|
||||||
tiller.tiller_namespace, tiller.timeout)
|
|
||||||
|
|
||||||
releases = {}
|
|
||||||
for release in tiller.list_releases():
|
|
||||||
releases.setdefault(release.namespace, [])
|
|
||||||
releases[release.namespace].append(release.name)
|
|
||||||
return releases
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ from armada.api.controller.test import TestReleasesReleaseNameController
|
||||||
from armada.api.controller.test import TestReleasesManifestController
|
from armada.api.controller.test import TestReleasesManifestController
|
||||||
from armada.api.controller.health import Health
|
from armada.api.controller.health import Health
|
||||||
from armada.api.controller.metrics import Metrics
|
from armada.api.controller.metrics import Metrics
|
||||||
from armada.api.controller.tiller import Release
|
from armada.api.controller.releases import Releases
|
||||||
from armada.api.controller.tiller import Status
|
from armada.api.controller.tiller import Status
|
||||||
from armada.api.controller.validation import Validate
|
from armada.api.controller.validation import Validate
|
||||||
from armada.api.controller.versions import Versions
|
from armada.api.controller.versions import Versions
|
||||||
|
@ -62,10 +62,10 @@ def create(enable_middleware=CONF.middleware):
|
||||||
url_routes_v1 = [
|
url_routes_v1 = [
|
||||||
(HEALTH_PATH, Health()),
|
(HEALTH_PATH, Health()),
|
||||||
('apply', Apply()),
|
('apply', Apply()),
|
||||||
('releases', Release()),
|
('releases', Releases()),
|
||||||
('status', Status()),
|
('status', Status()),
|
||||||
('tests', TestReleasesManifestController()),
|
('tests', TestReleasesManifestController()),
|
||||||
('test/{release}', TestReleasesReleaseNameController()),
|
('test/{namespace}/{release}', TestReleasesReleaseNameController()),
|
||||||
('validatedesign', Validate()),
|
('validatedesign', Validate()),
|
||||||
(METRICS_PATH, Metrics()),
|
(METRICS_PATH, Metrics()),
|
||||||
]
|
]
|
||||||
|
|
|
@ -23,7 +23,7 @@ from armada.handlers import metrics
|
||||||
from armada.handlers.armada import Armada
|
from armada.handlers.armada import Armada
|
||||||
from armada.handlers.document import ReferenceResolver
|
from armada.handlers.document import ReferenceResolver
|
||||||
from armada.handlers.lock import lock_and_thread
|
from armada.handlers.lock import lock_and_thread
|
||||||
from armada.handlers.tiller import Tiller
|
from armada.handlers.helm import Helm
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
@ -100,15 +100,6 @@ SHORT_DESC = "Command installs manifest charts."
|
||||||
multiple=True,
|
multiple=True,
|
||||||
type=str,
|
type=str,
|
||||||
default=[])
|
default=[])
|
||||||
@click.option('--tiller-host', help="Tiller host IP.", default=None)
|
|
||||||
@click.option(
|
|
||||||
'--tiller-port', help="Tiller host port.", type=int, default=None)
|
|
||||||
@click.option(
|
|
||||||
'--tiller-namespace',
|
|
||||||
'-tn',
|
|
||||||
help="Tiller namespace.",
|
|
||||||
type=str,
|
|
||||||
default=None)
|
|
||||||
@click.option(
|
@click.option(
|
||||||
'--timeout',
|
'--timeout',
|
||||||
help="Specifies time to wait for each chart to fully "
|
help="Specifies time to wait for each chart to fully "
|
||||||
|
@ -141,23 +132,20 @@ SHORT_DESC = "Command installs manifest charts."
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def apply_create(
|
def apply_create(
|
||||||
ctx, locations, api, disable_update_post, disable_update_pre,
|
ctx, locations, api, disable_update_post, disable_update_pre,
|
||||||
enable_chart_cleanup, metrics_output, use_doc_ref, set, tiller_host,
|
enable_chart_cleanup, metrics_output, use_doc_ref, set, timeout,
|
||||||
tiller_port, tiller_namespace, timeout, values, wait, target_manifest,
|
values, wait, target_manifest, bearer_token, debug):
|
||||||
bearer_token, debug):
|
|
||||||
CONF.debug = debug
|
CONF.debug = debug
|
||||||
ApplyManifest(
|
ApplyManifest(
|
||||||
ctx, locations, api, disable_update_post, disable_update_pre,
|
ctx, locations, api, disable_update_post, disable_update_pre,
|
||||||
enable_chart_cleanup, metrics_output, use_doc_ref, set, tiller_host,
|
enable_chart_cleanup, metrics_output, use_doc_ref, set, timeout,
|
||||||
tiller_port, tiller_namespace, timeout, values, wait, target_manifest,
|
values, wait, target_manifest, bearer_token).safe_invoke()
|
||||||
bearer_token).safe_invoke()
|
|
||||||
|
|
||||||
|
|
||||||
class ApplyManifest(CliAction):
|
class ApplyManifest(CliAction):
|
||||||
def __init__(
|
def __init__(
|
||||||
self, ctx, locations, api, disable_update_post, disable_update_pre,
|
self, ctx, locations, api, disable_update_post, disable_update_pre,
|
||||||
enable_chart_cleanup, metrics_output, use_doc_ref, set,
|
enable_chart_cleanup, metrics_output, use_doc_ref, set, timeout,
|
||||||
tiller_host, tiller_port, tiller_namespace, timeout, values, wait,
|
values, wait, target_manifest, bearer_token):
|
||||||
target_manifest, bearer_token):
|
|
||||||
super(ApplyManifest, self).__init__()
|
super(ApplyManifest, self).__init__()
|
||||||
self.ctx = ctx
|
self.ctx = ctx
|
||||||
# Filename can also be a URL reference
|
# Filename can also be a URL reference
|
||||||
|
@ -169,9 +157,6 @@ class ApplyManifest(CliAction):
|
||||||
self.metrics_output = metrics_output
|
self.metrics_output = metrics_output
|
||||||
self.use_doc_ref = use_doc_ref
|
self.use_doc_ref = use_doc_ref
|
||||||
self.set = set
|
self.set = set
|
||||||
self.tiller_host = tiller_host
|
|
||||||
self.tiller_port = tiller_port
|
|
||||||
self.tiller_namespace = tiller_namespace
|
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
self.values = values
|
self.values = values
|
||||||
self.wait = wait
|
self.wait = wait
|
||||||
|
@ -210,13 +195,10 @@ class ApplyManifest(CliAction):
|
||||||
return
|
return
|
||||||
|
|
||||||
if not self.ctx.obj.get('api', False):
|
if not self.ctx.obj.get('api', False):
|
||||||
with Tiller(tiller_host=self.tiller_host,
|
with Helm(bearer_token=self.bearer_token) as helm:
|
||||||
tiller_port=self.tiller_port,
|
|
||||||
tiller_namespace=self.tiller_namespace,
|
|
||||||
bearer_token=self.bearer_token) as tiller:
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
resp = self.handle(documents, tiller)
|
resp = self.handle(documents, helm)
|
||||||
self.output(resp)
|
self.output(resp)
|
||||||
finally:
|
finally:
|
||||||
if self.metrics_output:
|
if self.metrics_output:
|
||||||
|
@ -235,9 +217,6 @@ class ApplyManifest(CliAction):
|
||||||
'disable_update_post': self.disable_update_post,
|
'disable_update_post': self.disable_update_post,
|
||||||
'disable_update_pre': self.disable_update_pre,
|
'disable_update_pre': self.disable_update_pre,
|
||||||
'enable_chart_cleanup': self.enable_chart_cleanup,
|
'enable_chart_cleanup': self.enable_chart_cleanup,
|
||||||
'tiller_host': self.tiller_host,
|
|
||||||
'tiller_port': self.tiller_port,
|
|
||||||
'tiller_namespace': self.tiller_namespace,
|
|
||||||
'timeout': self.timeout,
|
'timeout': self.timeout,
|
||||||
'wait': self.wait
|
'wait': self.wait
|
||||||
}
|
}
|
||||||
|
@ -252,7 +231,7 @@ class ApplyManifest(CliAction):
|
||||||
self.output(resp.get('message'))
|
self.output(resp.get('message'))
|
||||||
|
|
||||||
@lock_and_thread()
|
@lock_and_thread()
|
||||||
def handle(self, documents, tiller):
|
def handle(self, documents, helm):
|
||||||
armada = Armada(
|
armada = Armada(
|
||||||
documents,
|
documents,
|
||||||
disable_update_pre=self.disable_update_pre,
|
disable_update_pre=self.disable_update_pre,
|
||||||
|
@ -261,7 +240,7 @@ class ApplyManifest(CliAction):
|
||||||
set_ovr=self.set,
|
set_ovr=self.set,
|
||||||
force_wait=self.wait,
|
force_wait=self.wait,
|
||||||
timeout=self.timeout,
|
timeout=self.timeout,
|
||||||
tiller=tiller,
|
helm=helm,
|
||||||
values=self.values,
|
values=self.values,
|
||||||
target_manifest=self.target_manifest)
|
target_manifest=self.target_manifest)
|
||||||
return armada.sync()
|
return armada.sync()
|
||||||
|
|
|
@ -21,7 +21,7 @@ from armada import const
|
||||||
from armada.handlers.lock import lock_and_thread
|
from armada.handlers.lock import lock_and_thread
|
||||||
from armada.handlers.manifest import Manifest
|
from armada.handlers.manifest import Manifest
|
||||||
from armada.handlers.test import Test
|
from armada.handlers.test import Test
|
||||||
from armada.handlers.tiller import Tiller
|
from armada.handlers.helm import Helm, HelmReleaseId
|
||||||
from armada.utils.release import release_prefixer
|
from armada.utils.release import release_prefixer
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
|
@ -37,7 +37,6 @@ def test():
|
||||||
DESC = """
|
DESC = """
|
||||||
This command tests deployed charts.
|
This command tests deployed charts.
|
||||||
|
|
||||||
The tiller command uses flags to obtain information from Tiller services.
|
|
||||||
The test command will run the release chart tests either via a the manifest or
|
The test command will run the release chart tests either via a the manifest or
|
||||||
by targeting a release.
|
by targeting a release.
|
||||||
|
|
||||||
|
@ -47,7 +46,7 @@ To test Armada deployed releases:
|
||||||
|
|
||||||
To test release:
|
To test release:
|
||||||
|
|
||||||
$ armada test --release blog-1
|
$ armada test --namespace blog --release blog-1
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -56,27 +55,14 @@ SHORT_DESC = "Command tests releases."
|
||||||
|
|
||||||
@test.command(name='test', help=DESC, short_help=SHORT_DESC)
|
@test.command(name='test', help=DESC, short_help=SHORT_DESC)
|
||||||
@click.option('--file', help="Armada manifest.", type=str)
|
@click.option('--file', help="Armada manifest.", type=str)
|
||||||
|
@click.option('--namespace', help="Helm release namespace.", type=str)
|
||||||
@click.option('--release', help="Helm release.", type=str)
|
@click.option('--release', help="Helm release.", type=str)
|
||||||
@click.option('--tiller-host', help="Tiller host IP.", default=None)
|
|
||||||
@click.option(
|
|
||||||
'--tiller-port', help="Tiller host port.", type=int, default=None)
|
|
||||||
@click.option(
|
|
||||||
'--tiller-namespace',
|
|
||||||
'-tn',
|
|
||||||
help="Tiller Namespace.",
|
|
||||||
type=str,
|
|
||||||
default=None)
|
|
||||||
@click.option(
|
@click.option(
|
||||||
'--target-manifest',
|
'--target-manifest',
|
||||||
help=(
|
help=(
|
||||||
"The target manifest to run. Required for specifying "
|
"The target manifest to run. Required for specifying "
|
||||||
"which manifest to run when multiple are available."),
|
"which manifest to run when multiple are available."),
|
||||||
default=None)
|
default=None)
|
||||||
@click.option(
|
|
||||||
'--cleanup',
|
|
||||||
help=("Delete test pods upon completion."),
|
|
||||||
is_flag=True,
|
|
||||||
default=None)
|
|
||||||
@click.option(
|
@click.option(
|
||||||
'--enable-all',
|
'--enable-all',
|
||||||
help=(
|
help=(
|
||||||
|
@ -87,54 +73,42 @@ SHORT_DESC = "Command tests releases."
|
||||||
@click.option('--debug', help="Enable debug logging.", is_flag=True)
|
@click.option('--debug', help="Enable debug logging.", is_flag=True)
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def test_charts(
|
def test_charts(
|
||||||
ctx, file, release, tiller_host, tiller_port, tiller_namespace,
|
ctx, file, namespace, release, target_manifest, enable_all, debug):
|
||||||
target_manifest, cleanup, enable_all, debug):
|
|
||||||
CONF.debug = debug
|
CONF.debug = debug
|
||||||
TestChartManifest(
|
TestChartManifest(
|
||||||
ctx, file, release, tiller_host, tiller_port, tiller_namespace,
|
ctx, file, namespace, release, target_manifest,
|
||||||
target_manifest, cleanup, enable_all).safe_invoke()
|
enable_all).safe_invoke()
|
||||||
|
|
||||||
|
|
||||||
class TestChartManifest(CliAction):
|
class TestChartManifest(CliAction):
|
||||||
def __init__(
|
def __init__(
|
||||||
self, ctx, file, release, tiller_host, tiller_port,
|
self, ctx, file, namespace, release, target_manifest, enable_all):
|
||||||
tiller_namespace, target_manifest, cleanup, enable_all):
|
|
||||||
|
|
||||||
super(TestChartManifest, self).__init__()
|
super(TestChartManifest, self).__init__()
|
||||||
self.ctx = ctx
|
self.ctx = ctx
|
||||||
self.file = file
|
self.file = file
|
||||||
|
self.namespace = namespace
|
||||||
self.release = release
|
self.release = release
|
||||||
self.tiller_host = tiller_host
|
|
||||||
self.tiller_port = tiller_port
|
|
||||||
self.tiller_namespace = tiller_namespace
|
|
||||||
self.target_manifest = target_manifest
|
self.target_manifest = target_manifest
|
||||||
self.cleanup = cleanup
|
|
||||||
self.enable_all = enable_all
|
self.enable_all = enable_all
|
||||||
|
|
||||||
def invoke(self):
|
def invoke(self):
|
||||||
with Tiller(tiller_host=self.tiller_host, tiller_port=self.tiller_port,
|
with Helm() as helm:
|
||||||
tiller_namespace=self.tiller_namespace) as tiller:
|
|
||||||
|
|
||||||
self.handle(tiller)
|
self.handle(helm)
|
||||||
|
|
||||||
@lock_and_thread()
|
@lock_and_thread()
|
||||||
def handle(self, tiller):
|
def handle(self, helm):
|
||||||
known_release_names = [release[0] for release in tiller.list_charts()]
|
release_ids = helm.list_release_ids()
|
||||||
|
|
||||||
if self.release:
|
if self.release:
|
||||||
if not self.ctx.obj.get('api', False):
|
if not self.ctx.obj.get('api', False):
|
||||||
test_handler = Test(
|
release_id = HelmReleaseId(self.namespace, self.release)
|
||||||
{}, self.release, tiller, cleanup=self.cleanup)
|
test_handler = Test({}, release_id, helm)
|
||||||
test_handler.test_release_for_success()
|
test_handler.test_release_for_success()
|
||||||
else:
|
else:
|
||||||
client = self.ctx.obj.get('CLIENT')
|
client = self.ctx.obj.get('CLIENT')
|
||||||
query = {
|
resp = client.get_test_release(release=self.release)
|
||||||
'tiller_host': self.tiller_host,
|
|
||||||
'tiller_port': self.tiller_port,
|
|
||||||
'tiller_namespace': self.tiller_namespace
|
|
||||||
}
|
|
||||||
resp = client.get_test_release(
|
|
||||||
release=self.release, query=query)
|
|
||||||
|
|
||||||
self.logger.info(resp.get('result'))
|
self.logger.info(resp.get('result'))
|
||||||
self.logger.info(resp.get('message'))
|
self.logger.info(resp.get('message'))
|
||||||
|
@ -153,33 +127,26 @@ class TestChartManifest(CliAction):
|
||||||
for ch in group.get(const.KEYWORD_CHARTS):
|
for ch in group.get(const.KEYWORD_CHARTS):
|
||||||
chart = ch['chart']
|
chart = ch['chart']
|
||||||
|
|
||||||
release_name = release_prefixer(
|
release_id = HelmReleaseId(
|
||||||
prefix, chart.get('release'))
|
chart['namespace'],
|
||||||
if release_name in known_release_names:
|
release_prefixer(prefix, chart['release']))
|
||||||
|
if release_id in release_ids:
|
||||||
test_handler = Test(
|
test_handler = Test(
|
||||||
chart,
|
chart,
|
||||||
release_name,
|
release_id,
|
||||||
tiller,
|
helm,
|
||||||
cleanup=self.cleanup,
|
|
||||||
enable_all=self.enable_all)
|
enable_all=self.enable_all)
|
||||||
|
|
||||||
if test_handler.test_enabled:
|
if test_handler.test_enabled:
|
||||||
test_handler.test_release_for_success()
|
test_handler.test_release_for_success()
|
||||||
else:
|
else:
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
'Release %s not found - SKIPPING',
|
'Release %s not found - SKIPPING', release_id)
|
||||||
release_name)
|
|
||||||
else:
|
else:
|
||||||
client = self.ctx.obj.get('CLIENT')
|
client = self.ctx.obj.get('CLIENT')
|
||||||
query = {
|
|
||||||
'tiller_host': self.tiller_host,
|
|
||||||
'tiller_port': self.tiller_port,
|
|
||||||
'tiller_namespace': self.tiller_namespace
|
|
||||||
}
|
|
||||||
|
|
||||||
with open(self.filename, 'r') as f:
|
with open(self.filename, 'r') as f:
|
||||||
resp = client.get_test_manifest(
|
resp = client.get_test_manifest(manifest=f.read())
|
||||||
manifest=f.read(), query=query)
|
|
||||||
for test in resp.get('tests'):
|
for test in resp.get('tests'):
|
||||||
self.logger.info('Test State: %s', test)
|
self.logger.info('Test State: %s', test)
|
||||||
for item in test.get('tests').get(test):
|
for item in test.get('tests').get(test):
|
||||||
|
|
|
@ -47,6 +47,14 @@ armada_policies = [
|
||||||
'path': '/api/v1.0/tests/',
|
'path': '/api/v1.0/tests/',
|
||||||
'method': 'POST'
|
'method': 'POST'
|
||||||
}]),
|
}]),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=base.ARMADA % 'get_release',
|
||||||
|
check_str=base.RULE_ADMIN_VIEWER,
|
||||||
|
description='Get helm releases',
|
||||||
|
operations=[{
|
||||||
|
'path': '/api/v1.0/releases/',
|
||||||
|
'method': 'GET'
|
||||||
|
}]),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -23,14 +23,6 @@ tiller_policies = [
|
||||||
'path': '/api/v1.0/status/',
|
'path': '/api/v1.0/status/',
|
||||||
'method': 'GET'
|
'method': 'GET'
|
||||||
}]),
|
}]),
|
||||||
policy.DocumentedRuleDefault(
|
|
||||||
name=base.TILLER % 'get_release',
|
|
||||||
check_str=base.RULE_ADMIN_VIEWER,
|
|
||||||
description='Get Tiller release',
|
|
||||||
operations=[{
|
|
||||||
'path': '/api/v1.0/releases/',
|
|
||||||
'method': 'GET'
|
|
||||||
}]),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -36,10 +36,10 @@ class ProtectedReleaseException(ArmadaException):
|
||||||
`continue_processing` is False.
|
`continue_processing` is False.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def __init__(self, release, status):
|
def __init__(self, release_id, status):
|
||||||
self._message = (
|
self._message = (
|
||||||
'Armada encountered protected release {} in {} status'.format(
|
'Armada encountered protected release {} in {} status'.format(
|
||||||
release, status))
|
release_id, status))
|
||||||
super(ProtectedReleaseException, self).__init__(self._message)
|
super(ProtectedReleaseException, self).__init__(self._message)
|
||||||
|
|
||||||
|
|
||||||
|
@ -92,15 +92,15 @@ class WaitException(ArmadaException):
|
||||||
class DeploymentLikelyPendingException(ArmadaException):
|
class DeploymentLikelyPendingException(ArmadaException):
|
||||||
'''
|
'''
|
||||||
Exception that occurs when it is detected that an existing release
|
Exception that occurs when it is detected that an existing release
|
||||||
operation (e.g. install, update, rollback, delete) is likely still pending.
|
operation (e.g. install, update, delete) is likely still pending.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def __init__(self, release, status, last_deployment_age, timeout):
|
def __init__(self, release_id, status, last_deployment_age, timeout):
|
||||||
self._message = (
|
self._message = (
|
||||||
'Existing deployment likely pending '
|
'Existing deployment likely pending '
|
||||||
'release={}, status={}, '
|
'release={}, status={}, '
|
||||||
'(last deployment age={}s) < (chart wait timeout={}s)'.format(
|
'(last deployment age={}s) < (chart wait timeout={}s)'.format(
|
||||||
release, status, last_deployment_age, timeout))
|
release_id, status, last_deployment_age, timeout))
|
||||||
super(DeploymentLikelyPendingException, self).__init__(self._message)
|
super(DeploymentLikelyPendingException, self).__init__(self._message)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -21,28 +21,9 @@ class ChartBuilderException(base_exception.ArmadaBaseException):
|
||||||
message = 'An unknown Armada handler error occurred.'
|
message = 'An unknown Armada handler error occurred.'
|
||||||
|
|
||||||
|
|
||||||
class DependencyException(ChartBuilderException):
|
|
||||||
'''
|
|
||||||
Exception that occurs when dependencies cannot be resolved.
|
|
||||||
|
|
||||||
**Troubleshoot:**
|
|
||||||
*Coming Soon*
|
|
||||||
'''
|
|
||||||
|
|
||||||
def __init__(self, chart_name):
|
|
||||||
self._chart_name = chart_name
|
|
||||||
self._message = 'Failed to resolve dependencies for ' + \
|
|
||||||
self._chart_name + '.'
|
|
||||||
|
|
||||||
super(DependencyException, self).__init__(self._message)
|
|
||||||
|
|
||||||
|
|
||||||
class HelmChartBuildException(ChartBuilderException):
|
class HelmChartBuildException(ChartBuilderException):
|
||||||
'''
|
'''
|
||||||
Exception that occurs when Helm Chart fails to build.
|
Exception that occurs when Helm Chart fails to build.
|
||||||
|
|
||||||
**Troubleshoot:**
|
|
||||||
*Coming Soon*
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def __init__(self, chart_name, details):
|
def __init__(self, chart_name, details):
|
||||||
|
@ -56,54 +37,3 @@ class HelmChartBuildException(ChartBuilderException):
|
||||||
}))
|
}))
|
||||||
|
|
||||||
super(HelmChartBuildException, self).__init__(self._message)
|
super(HelmChartBuildException, self).__init__(self._message)
|
||||||
|
|
||||||
|
|
||||||
class FilesLoadException(ChartBuilderException):
|
|
||||||
'''
|
|
||||||
Exception that occurs while trying to read a file in the chart directory.
|
|
||||||
|
|
||||||
**Troubleshoot:**
|
|
||||||
|
|
||||||
* Ensure that the file can be encoded to utf-8 or else it cannot be parsed.
|
|
||||||
'''
|
|
||||||
|
|
||||||
message = (
|
|
||||||
'A %(clazz)s exception occurred while trying to read '
|
|
||||||
'file: %(file)s. Details:\n%(details)s')
|
|
||||||
|
|
||||||
|
|
||||||
class IgnoredFilesLoadException(ChartBuilderException):
|
|
||||||
'''
|
|
||||||
Exception that occurs when there is an error loading files contained in
|
|
||||||
.helmignore.
|
|
||||||
|
|
||||||
**Troubleshoot:**
|
|
||||||
*Coming Soon*
|
|
||||||
'''
|
|
||||||
|
|
||||||
message = 'An error occurred while loading the ignored files in \
|
|
||||||
.helmignore'
|
|
||||||
|
|
||||||
|
|
||||||
class MetadataLoadException(ChartBuilderException):
|
|
||||||
'''
|
|
||||||
Exception that occurs when metadata loading fails.
|
|
||||||
|
|
||||||
**Troubleshoot:**
|
|
||||||
*Coming Soon*
|
|
||||||
'''
|
|
||||||
|
|
||||||
message = 'Failed to load metadata from chart yaml file'
|
|
||||||
|
|
||||||
|
|
||||||
class UnknownChartSourceException(ChartBuilderException):
|
|
||||||
'''Exception for unknown chart source type.'''
|
|
||||||
|
|
||||||
def __init__(self, chart_name, source_type):
|
|
||||||
self._chart_name = chart_name
|
|
||||||
self._source_type = source_type
|
|
||||||
|
|
||||||
self._message = 'Unknown source type \"' + self._source_type + '\" for \
|
|
||||||
chart \"' + self._chart_name + '\"'
|
|
||||||
|
|
||||||
super(UnknownChartSourceException, self).__init__(self._message)
|
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Copyright 2021 The Armada Authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from armada.exceptions.base_exception import ArmadaBaseException as ex
|
||||||
|
|
||||||
|
|
||||||
|
class HelmCommandException(ex):
|
||||||
|
'''
|
||||||
|
Exception that occurs when a helm command fails.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, called_process_error):
|
||||||
|
self.called_process_error = called_process_error
|
||||||
|
message = 'helm command failed: {}'.format(
|
||||||
|
self.called_process_error.stderr)
|
||||||
|
super(HelmCommandException, self).__init__(message)
|
|
@ -21,17 +21,6 @@ class TillerException(ex):
|
||||||
message = 'An unknown Tiller error occurred.'
|
message = 'An unknown Tiller error occurred.'
|
||||||
|
|
||||||
|
|
||||||
class TillerServicesUnavailableException(TillerException):
|
|
||||||
'''
|
|
||||||
Exception for tiller service being unavailable.
|
|
||||||
|
|
||||||
**Troubleshoot:**
|
|
||||||
*Coming Soon*
|
|
||||||
'''
|
|
||||||
|
|
||||||
message = 'Tiller services unavailable.'
|
|
||||||
|
|
||||||
|
|
||||||
class ChartCleanupException(TillerException):
|
class ChartCleanupException(TillerException):
|
||||||
'''Exception that occurs during chart cleanup.'''
|
'''Exception that occurs during chart cleanup.'''
|
||||||
|
|
||||||
|
|
|
@ -21,11 +21,11 @@ from armada import const
|
||||||
from armada.conf import set_current_chart
|
from armada.conf import set_current_chart
|
||||||
from armada.exceptions import armada_exceptions
|
from armada.exceptions import armada_exceptions
|
||||||
from armada.exceptions import override_exceptions
|
from armada.exceptions import override_exceptions
|
||||||
from armada.exceptions import tiller_exceptions
|
|
||||||
from armada.exceptions import validate_exceptions
|
from armada.exceptions import validate_exceptions
|
||||||
from armada.handlers import metrics
|
from armada.handlers import metrics
|
||||||
from armada.handlers.chart_deploy import ChartDeploy
|
from armada.handlers.chart_deploy import ChartDeploy
|
||||||
from armada.handlers.chart_download import ChartDownload
|
from armada.handlers.chart_download import ChartDownload
|
||||||
|
from armada.handlers.helm import HelmReleaseId
|
||||||
from armada.handlers.manifest import Manifest
|
from armada.handlers.manifest import Manifest
|
||||||
from armada.handlers.override import Override
|
from armada.handlers.override import Override
|
||||||
from armada.utils.release import release_prefixer
|
from armada.utils.release import release_prefixer
|
||||||
|
@ -43,7 +43,7 @@ class Armada(object):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
documents,
|
documents,
|
||||||
tiller,
|
helm,
|
||||||
disable_update_pre=False,
|
disable_update_pre=False,
|
||||||
disable_update_post=False,
|
disable_update_post=False,
|
||||||
enable_chart_cleanup=False,
|
enable_chart_cleanup=False,
|
||||||
|
@ -55,18 +55,16 @@ class Armada(object):
|
||||||
k8s_wait_attempts=1,
|
k8s_wait_attempts=1,
|
||||||
k8s_wait_attempt_sleep=1):
|
k8s_wait_attempt_sleep=1):
|
||||||
'''
|
'''
|
||||||
Initialize the Armada engine and establish a connection to Tiller.
|
Initialize the Armada engine.
|
||||||
|
|
||||||
:param List[dict] documents: Armada documents.
|
:param List[dict] documents: Armada documents.
|
||||||
:param tiller: Tiller instance to use.
|
:param bool disable_update_pre: Disable pre-update operations.
|
||||||
:param bool disable_update_pre: Disable pre-update Tiller operations.
|
:param bool disable_update_post: Disable post-update operations.
|
||||||
:param bool disable_update_post: Disable post-update Tiller
|
|
||||||
operations.
|
|
||||||
:param bool enable_chart_cleanup: Clean up unmanaged charts.
|
:param bool enable_chart_cleanup: Clean up unmanaged charts.
|
||||||
:param bool force_wait: Force Tiller to wait until all charts are
|
:param bool force_wait: Force to wait until all charts are
|
||||||
deployed, rather than using each chart's specified wait policy.
|
deployed, rather than using each chart's specified wait policy.
|
||||||
:param int timeout: Specifies overall time in seconds that Tiller
|
:param int timeout: Specifies overall time in seconds t to wait for
|
||||||
should wait for charts until timing out.
|
charts until timing out.
|
||||||
:param str target_manifest: The target manifest to run. Useful for
|
:param str target_manifest: The target manifest to run. Useful for
|
||||||
specifying which manifest to run when multiple are available.
|
specifying which manifest to run when multiple are available.
|
||||||
:param int k8s_wait_attempts: The number of times to attempt waiting
|
:param int k8s_wait_attempts: The number of times to attempt waiting
|
||||||
|
@ -77,7 +75,7 @@ class Armada(object):
|
||||||
|
|
||||||
self.enable_chart_cleanup = enable_chart_cleanup
|
self.enable_chart_cleanup = enable_chart_cleanup
|
||||||
self.force_wait = force_wait
|
self.force_wait = force_wait
|
||||||
self.tiller = tiller
|
self.helm = helm
|
||||||
try:
|
try:
|
||||||
self.documents = Override(
|
self.documents = Override(
|
||||||
documents, overrides=set_ovr,
|
documents, overrides=set_ovr,
|
||||||
|
@ -90,7 +88,7 @@ class Armada(object):
|
||||||
self.chart_download = ChartDownload()
|
self.chart_download = ChartDownload()
|
||||||
self.chart_deploy = ChartDeploy(
|
self.chart_deploy = ChartDeploy(
|
||||||
self.manifest, disable_update_pre, disable_update_post,
|
self.manifest, disable_update_pre, disable_update_post,
|
||||||
k8s_wait_attempts, k8s_wait_attempt_sleep, timeout, self.tiller)
|
k8s_wait_attempts, k8s_wait_attempt_sleep, timeout, self.helm)
|
||||||
|
|
||||||
def pre_flight_ops(self):
|
def pre_flight_ops(self):
|
||||||
"""Perform a series of checks and operations to ensure proper
|
"""Perform a series of checks and operations to ensure proper
|
||||||
|
@ -98,10 +96,6 @@ class Armada(object):
|
||||||
"""
|
"""
|
||||||
LOG.info("Performing pre-flight operations.")
|
LOG.info("Performing pre-flight operations.")
|
||||||
|
|
||||||
# Ensure Tiller is available and manifest is valid
|
|
||||||
if not self.tiller.tiller_status():
|
|
||||||
raise tiller_exceptions.TillerServicesUnavailableException()
|
|
||||||
|
|
||||||
# Clone the chart sources
|
# Clone the chart sources
|
||||||
manifest_data = self.manifest.get(const.KEYWORD_DATA, {})
|
manifest_data = self.manifest.get(const.KEYWORD_DATA, {})
|
||||||
for group in manifest_data.get(const.KEYWORD_GROUPS, []):
|
for group in manifest_data.get(const.KEYWORD_GROUPS, []):
|
||||||
|
@ -130,8 +124,6 @@ class Armada(object):
|
||||||
# a more cleaner format
|
# a more cleaner format
|
||||||
self.pre_flight_ops()
|
self.pre_flight_ops()
|
||||||
|
|
||||||
known_releases = self.tiller.list_releases()
|
|
||||||
|
|
||||||
manifest_data = self.manifest.get(const.KEYWORD_DATA, {})
|
manifest_data = self.manifest.get(const.KEYWORD_DATA, {})
|
||||||
prefix = manifest_data.get(const.KEYWORD_PREFIX)
|
prefix = manifest_data.get(const.KEYWORD_PREFIX)
|
||||||
|
|
||||||
|
@ -155,8 +147,7 @@ class Armada(object):
|
||||||
set_current_chart(chart)
|
set_current_chart(chart)
|
||||||
try:
|
try:
|
||||||
return self.chart_deploy.execute(
|
return self.chart_deploy.execute(
|
||||||
chart, cg_test_all_charts, prefix, known_releases,
|
chart, cg_test_all_charts, prefix, concurrency)
|
||||||
concurrency)
|
|
||||||
finally:
|
finally:
|
||||||
set_current_chart(None)
|
set_current_chart(None)
|
||||||
|
|
||||||
|
@ -225,20 +216,22 @@ class Armada(object):
|
||||||
def _chart_cleanup(self, prefix, chart_groups, msg):
|
def _chart_cleanup(self, prefix, chart_groups, msg):
|
||||||
LOG.info('Processing chart cleanup to remove unspecified releases.')
|
LOG.info('Processing chart cleanup to remove unspecified releases.')
|
||||||
|
|
||||||
valid_releases = []
|
valid_release_ids = []
|
||||||
for group in chart_groups:
|
for group in chart_groups:
|
||||||
group_data = group.get(const.KEYWORD_DATA, {})
|
group_data = group.get(const.KEYWORD_DATA, {})
|
||||||
for chart in group_data.get(const.KEYWORD_CHARTS, []):
|
for chart in group_data.get(const.KEYWORD_CHARTS, []):
|
||||||
chart_data = chart.get(const.KEYWORD_DATA, {})
|
chart_data = chart.get(const.KEYWORD_DATA, {})
|
||||||
valid_releases.append(
|
valid_release_ids.append(
|
||||||
release_prefixer(prefix, chart_data.get('release')))
|
HelmReleaseId(
|
||||||
|
chart_data['namespace'],
|
||||||
|
release_prefixer(prefix, chart_data['release'])))
|
||||||
|
|
||||||
actual_releases = [x.name for x in self.tiller.list_releases()]
|
actual_release_ids = self.helm.list_release_ids()
|
||||||
release_diff = list(set(actual_releases) - set(valid_releases))
|
release_diff = list(set(actual_release_ids) - set(valid_release_ids))
|
||||||
|
|
||||||
for release in release_diff:
|
for release_id in release_diff:
|
||||||
if release.startswith(prefix):
|
if release_id.name.startswith(prefix):
|
||||||
LOG.info(
|
LOG.info(
|
||||||
'Purging release %s as part of chart cleanup.', release)
|
'Purging release %s as part of chart cleanup.', release_id)
|
||||||
self.tiller.uninstall_release(release)
|
self.helm.uninstall_release(release_id)
|
||||||
msg['purge'].append(release)
|
msg['purge'].append('{}'.format(release_id))
|
||||||
|
|
|
@ -16,23 +16,23 @@ from armada import const
|
||||||
|
|
||||||
|
|
||||||
class ChartDelete(object):
|
class ChartDelete(object):
|
||||||
def __init__(self, chart, release_name, tiller, purge=True):
|
def __init__(self, chart, release_id, helm, purge=True):
|
||||||
"""Initialize a chart delete handler.
|
"""Initialize a chart delete handler.
|
||||||
|
|
||||||
:param chart: The armada chart document
|
:param chart: The armada chart document
|
||||||
:param release_name: Name of a Helm release
|
:param release_id: HelmReleaseId
|
||||||
:param tiller: Tiller object
|
:param helm: Helm object
|
||||||
:param purge: Whether to purge the release
|
:param purge: Whether to purge the release
|
||||||
|
|
||||||
:type chart: object
|
:type chart: object
|
||||||
:type release_name: str
|
:type release_name: str
|
||||||
:type tiller: Tiller object
|
:type helm: Helm object
|
||||||
:type purge: bool
|
:type purge: bool
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.chart = chart
|
self.chart = chart
|
||||||
self.release_name = release_name
|
self.release_id = release_id
|
||||||
self.tiller = tiller
|
self.helm = helm
|
||||||
self.purge = purge
|
self.purge = purge
|
||||||
self.delete_config = self.chart.get('delete', {})
|
self.delete_config = self.chart.get('delete', {})
|
||||||
# TODO(seaneagan): Consider allowing this to be a percentage of the
|
# TODO(seaneagan): Consider allowing this to be a percentage of the
|
||||||
|
@ -47,5 +47,5 @@ class ChartDelete(object):
|
||||||
def delete(self):
|
def delete(self):
|
||||||
"""Delete the release associated with the chart"
|
"""Delete the release associated with the chart"
|
||||||
"""
|
"""
|
||||||
self.tiller.uninstall_release(
|
self.helm.uninstall_release(
|
||||||
self.release_name, timeout=self.get_timeout(), purge=self.purge)
|
self.release_id, timeout=self.get_timeout(), purge=self.purge)
|
||||||
|
|
|
@ -12,22 +12,22 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
import os
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
import yaml
|
|
||||||
|
|
||||||
from armada import const
|
from armada import const
|
||||||
from armada.exceptions import armada_exceptions
|
from armada.exceptions import armada_exceptions
|
||||||
from armada.handlers import metrics
|
from armada.handlers import metrics
|
||||||
from armada.handlers.chartbuilder import ChartBuilder
|
from armada.handlers.chartbuilder import ChartBuilder
|
||||||
|
from armada.handlers import helm
|
||||||
from armada.handlers.release_diff import ReleaseDiff
|
from armada.handlers.release_diff import ReleaseDiff
|
||||||
from armada.handlers.chart_delete import ChartDelete
|
from armada.handlers.chart_delete import ChartDelete
|
||||||
from armada.handlers.pre_update_actions import PreUpdateActions
|
from armada.handlers.pre_update_actions import PreUpdateActions
|
||||||
from armada.handlers.schema import get_schema_info
|
from armada.handlers.schema import get_schema_info
|
||||||
from armada.handlers.test import Test
|
from armada.handlers.test import Test
|
||||||
from armada.handlers.wait import ChartWait
|
from armada.handlers.wait import ChartWait
|
||||||
from armada.exceptions import tiller_exceptions as tiller_ex
|
|
||||||
import armada.utils.release as r
|
import armada.utils.release as r
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
@ -36,40 +36,40 @@ LOG = logging.getLogger(__name__)
|
||||||
class ChartDeploy(object):
|
class ChartDeploy(object):
|
||||||
def __init__(
|
def __init__(
|
||||||
self, manifest, disable_update_pre, disable_update_post,
|
self, manifest, disable_update_pre, disable_update_post,
|
||||||
k8s_wait_attempts, k8s_wait_attempt_sleep, timeout, tiller):
|
k8s_wait_attempts, k8s_wait_attempt_sleep, timeout, helm):
|
||||||
self.manifest = manifest
|
self.manifest = manifest
|
||||||
self.disable_update_pre = disable_update_pre
|
self.disable_update_pre = disable_update_pre
|
||||||
self.disable_update_post = disable_update_post
|
self.disable_update_post = disable_update_post
|
||||||
self.k8s_wait_attempts = k8s_wait_attempts
|
self.k8s_wait_attempts = k8s_wait_attempts
|
||||||
self.k8s_wait_attempt_sleep = k8s_wait_attempt_sleep
|
self.k8s_wait_attempt_sleep = k8s_wait_attempt_sleep
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
self.tiller = tiller
|
self.helm = helm
|
||||||
|
|
||||||
def execute(
|
def execute(self, ch, cg_test_all_charts, prefix, concurrency):
|
||||||
self, ch, cg_test_all_charts, prefix, known_releases, concurrency):
|
|
||||||
chart_name = ch['metadata']['name']
|
chart_name = ch['metadata']['name']
|
||||||
manifest_name = self.manifest['metadata']['name']
|
manifest_name = self.manifest['metadata']['name']
|
||||||
with metrics.CHART_HANDLE.get_context(concurrency, manifest_name,
|
with metrics.CHART_HANDLE.get_context(concurrency, manifest_name,
|
||||||
chart_name):
|
chart_name):
|
||||||
return self._execute(
|
return self._execute(ch, cg_test_all_charts, prefix)
|
||||||
ch, cg_test_all_charts, prefix, known_releases)
|
|
||||||
|
|
||||||
def _execute(self, ch, cg_test_all_charts, prefix, known_releases):
|
def _execute(self, ch, cg_test_all_charts, prefix):
|
||||||
manifest_name = self.manifest['metadata']['name']
|
manifest_name = self.manifest['metadata']['name']
|
||||||
chart = ch[const.KEYWORD_DATA]
|
chart = ch[const.KEYWORD_DATA]
|
||||||
chart_name = ch['metadata']['name']
|
chart_name = ch['metadata']['name']
|
||||||
namespace = chart.get('namespace')
|
namespace = chart.get('namespace')
|
||||||
release = chart.get('release')
|
release = chart.get('release')
|
||||||
release_name = r.release_prefixer(prefix, release)
|
release_name = r.release_prefixer(prefix, release)
|
||||||
LOG.info('Processing Chart, release=%s', release_name)
|
release_id = helm.HelmReleaseId(namespace, release_name)
|
||||||
|
source_dir = chart['source_dir']
|
||||||
|
source_directory = os.path.join(*source_dir)
|
||||||
|
LOG.info('Processing Chart, release=%s', release_id)
|
||||||
|
|
||||||
result = {}
|
result = {}
|
||||||
|
|
||||||
chart_wait = ChartWait(
|
chart_wait = ChartWait(
|
||||||
self.tiller.k8s,
|
self.helm.k8s,
|
||||||
release_name,
|
release_id,
|
||||||
ch,
|
ch,
|
||||||
namespace,
|
|
||||||
k8s_wait_attempts=self.k8s_wait_attempts,
|
k8s_wait_attempts=self.k8s_wait_attempts,
|
||||||
k8s_wait_attempt_sleep=self.k8s_wait_attempt_sleep,
|
k8s_wait_attempt_sleep=self.k8s_wait_attempt_sleep,
|
||||||
timeout=self.timeout)
|
timeout=self.timeout)
|
||||||
|
@ -77,7 +77,7 @@ class ChartDeploy(object):
|
||||||
|
|
||||||
# Begin Chart timeout deadline
|
# Begin Chart timeout deadline
|
||||||
deadline = time.time() + wait_timeout
|
deadline = time.time() + wait_timeout
|
||||||
old_release = self.find_chart_release(known_releases, release_name)
|
old_release = self.helm.release_metadata(release_id)
|
||||||
action = metrics.ChartDeployAction.NOOP
|
action = metrics.ChartDeployAction.NOOP
|
||||||
|
|
||||||
def noop():
|
def noop():
|
||||||
|
@ -95,20 +95,17 @@ class ChartDeploy(object):
|
||||||
|
|
||||||
native_wait_enabled = chart_wait.is_native_enabled()
|
native_wait_enabled = chart_wait.is_native_enabled()
|
||||||
|
|
||||||
chartbuilder = ChartBuilder.from_chart_doc(ch)
|
chartbuilder = ChartBuilder.from_chart_doc(ch, self.helm)
|
||||||
new_chart = chartbuilder.get_helm_chart()
|
|
||||||
|
|
||||||
if status == const.STATUS_DEPLOYED:
|
if status == helm.STATUS_DEPLOYED:
|
||||||
|
|
||||||
# indicate to the end user what path we are taking
|
# indicate to the end user what path we are taking
|
||||||
LOG.info(
|
LOG.info("Existing release %s found", release_id)
|
||||||
"Existing release %s found in namespace %s", release_name,
|
|
||||||
namespace)
|
|
||||||
|
|
||||||
# extract the installed chart and installed values from the
|
# extract the installed chart and installed values from the
|
||||||
# latest release so we can compare to the intended state
|
# latest release so we can compare to the intended state
|
||||||
old_chart = old_release.chart
|
old_chart = old_release['chart']
|
||||||
old_values_string = old_release.config.raw
|
old_values = old_release.get('config', {})
|
||||||
|
|
||||||
upgrade = chart.get('upgrade', {})
|
upgrade = chart.get('upgrade', {})
|
||||||
options = upgrade.get('options', {})
|
options = upgrade.get('options', {})
|
||||||
|
@ -122,7 +119,6 @@ class ChartDeploy(object):
|
||||||
|
|
||||||
disable_hooks = no_hooks_location.get('no_hooks', False)
|
disable_hooks = no_hooks_location.get('no_hooks', False)
|
||||||
force = options.get('force', False)
|
force = options.get('force', False)
|
||||||
recreate_pods = options.get('recreate_pods', False)
|
|
||||||
|
|
||||||
if upgrade:
|
if upgrade:
|
||||||
upgrade_pre = upgrade.get('pre', {})
|
upgrade_pre = upgrade.get('pre', {})
|
||||||
|
@ -136,15 +132,8 @@ class ChartDeploy(object):
|
||||||
'Post upgrade actions are ignored by Armada'
|
'Post upgrade actions are ignored by Armada'
|
||||||
'and will not affect deployment.')
|
'and will not affect deployment.')
|
||||||
|
|
||||||
try:
|
|
||||||
old_values = yaml.safe_load(old_values_string)
|
|
||||||
except yaml.YAMLError:
|
|
||||||
chart_desc = '{} (previously deployed)'.format(
|
|
||||||
old_chart.metadata.name)
|
|
||||||
raise armada_exceptions.\
|
|
||||||
InvalidOverrideValuesYamlException(chart_desc)
|
|
||||||
|
|
||||||
LOG.info('Checking for updates to chart release inputs.')
|
LOG.info('Checking for updates to chart release inputs.')
|
||||||
|
new_chart = chartbuilder.get_helm_chart(release_id, values)
|
||||||
diff = self.get_diff(old_chart, old_values, new_chart, values)
|
diff = self.get_diff(old_chart, old_values, new_chart, values)
|
||||||
|
|
||||||
if not diff:
|
if not diff:
|
||||||
|
@ -152,34 +141,29 @@ class ChartDeploy(object):
|
||||||
else:
|
else:
|
||||||
action = metrics.ChartDeployAction.UPGRADE
|
action = metrics.ChartDeployAction.UPGRADE
|
||||||
LOG.info("Found updates to chart release inputs")
|
LOG.info("Found updates to chart release inputs")
|
||||||
LOG.debug("Release=%s, diff=%s", release_name, diff)
|
LOG.debug("Release=%s, diff=%s", release_id, diff)
|
||||||
result['diff'] = {chart['release']: str(diff)}
|
result['diff'] = {chart['release']: str(diff)}
|
||||||
|
|
||||||
def upgrade():
|
def upgrade():
|
||||||
# do actual update
|
# do actual update
|
||||||
timer = int(round(deadline - time.time()))
|
timer = int(round(deadline - time.time()))
|
||||||
PreUpdateActions(self.tiller.k8s).execute(
|
PreUpdateActions(self.helm.k8s).execute(
|
||||||
pre_actions, release, namespace, chart, disable_hooks,
|
pre_actions, release, namespace, chart, disable_hooks,
|
||||||
values, timer)
|
values, timer)
|
||||||
LOG.info(
|
LOG.info(
|
||||||
"Upgrading release %s in namespace %s, wait=%s, "
|
"Upgrading release=%s, wait=%s, "
|
||||||
"timeout=%ss", release_name, namespace,
|
"timeout=%ss", release_id, native_wait_enabled, timer)
|
||||||
native_wait_enabled, timer)
|
self.helm.upgrade_release(
|
||||||
tiller_result = self.tiller.update_release(
|
source_directory,
|
||||||
new_chart,
|
release_id,
|
||||||
release_name,
|
|
||||||
namespace,
|
|
||||||
disable_hooks=disable_hooks,
|
disable_hooks=disable_hooks,
|
||||||
values=yaml.safe_dump(values),
|
values=values,
|
||||||
wait=native_wait_enabled,
|
wait=native_wait_enabled,
|
||||||
timeout=timer,
|
timeout=timer,
|
||||||
force=force,
|
force=force)
|
||||||
recreate_pods=recreate_pods)
|
|
||||||
|
|
||||||
LOG.info(
|
LOG.info('Upgrade completed')
|
||||||
'Upgrade completed with results from Tiller: %s',
|
result['upgrade'] = release_id
|
||||||
tiller_result.__dict__)
|
|
||||||
result['upgrade'] = release_name
|
|
||||||
|
|
||||||
deploy = upgrade
|
deploy = upgrade
|
||||||
else:
|
else:
|
||||||
|
@ -187,28 +171,24 @@ class ChartDeploy(object):
|
||||||
def install():
|
def install():
|
||||||
timer = int(round(deadline - time.time()))
|
timer = int(round(deadline - time.time()))
|
||||||
LOG.info(
|
LOG.info(
|
||||||
"Installing release %s in namespace %s, wait=%s, "
|
"Installing release=%s, wait=%s, "
|
||||||
"timeout=%ss", release_name, namespace,
|
"timeout=%ss", release_id, native_wait_enabled, timer)
|
||||||
native_wait_enabled, timer)
|
self.helm.install_release(
|
||||||
tiller_result = self.tiller.install_release(
|
source_directory,
|
||||||
new_chart,
|
release_id,
|
||||||
release_name,
|
values=values,
|
||||||
namespace,
|
|
||||||
values=yaml.safe_dump(values),
|
|
||||||
wait=native_wait_enabled,
|
wait=native_wait_enabled,
|
||||||
timeout=timer)
|
timeout=timer)
|
||||||
|
|
||||||
LOG.info(
|
LOG.info('Install completed')
|
||||||
'Install completed with results from Tiller: %s',
|
result['install'] = release_id
|
||||||
tiller_result.__dict__)
|
|
||||||
result['install'] = release_name
|
|
||||||
|
|
||||||
# Check for release with status other than DEPLOYED
|
# Check for release with status other than DEPLOYED
|
||||||
if status:
|
if status:
|
||||||
if status != const.STATUS_FAILED:
|
if status != helm.STATUS_FAILED:
|
||||||
LOG.warn(
|
LOG.warn(
|
||||||
'Unexpected release status encountered '
|
'Unexpected release status encountered '
|
||||||
'release=%s, status=%s', release_name, status)
|
'release=%s, status=%s', release_id, status)
|
||||||
|
|
||||||
# Make best effort to determine whether a deployment is
|
# Make best effort to determine whether a deployment is
|
||||||
# likely pending, by checking if the last deployment
|
# likely pending, by checking if the last deployment
|
||||||
|
@ -222,7 +202,7 @@ class ChartDeploy(object):
|
||||||
deploy = noop
|
deploy = noop
|
||||||
deadline = deadline - last_deployment_age
|
deadline = deadline - last_deployment_age
|
||||||
else:
|
else:
|
||||||
# Release is likely stuck in an unintended (by tiller)
|
# Release is likely stuck in an unintended
|
||||||
# state. Log and continue on with remediation steps
|
# state. Log and continue on with remediation steps
|
||||||
# below.
|
# below.
|
||||||
LOG.info(
|
LOG.info(
|
||||||
|
@ -231,7 +211,7 @@ class ChartDeploy(object):
|
||||||
'(chart wait timeout=%ss)', release, status,
|
'(chart wait timeout=%ss)', release, status,
|
||||||
last_deployment_age, wait_timeout)
|
last_deployment_age, wait_timeout)
|
||||||
res = self.purge_release(
|
res = self.purge_release(
|
||||||
chart, release_name, status, manifest_name,
|
chart, release_id, status, manifest_name,
|
||||||
chart_name, result)
|
chart_name, result)
|
||||||
if isinstance(res, dict):
|
if isinstance(res, dict):
|
||||||
if 'protected' in res:
|
if 'protected' in res:
|
||||||
|
@ -242,7 +222,7 @@ class ChartDeploy(object):
|
||||||
# The chart is in Failed state, hence we purge
|
# The chart is in Failed state, hence we purge
|
||||||
# the chart and attempt to install it again.
|
# the chart and attempt to install it again.
|
||||||
res = self.purge_release(
|
res = self.purge_release(
|
||||||
chart, release_name, status, manifest_name, chart_name,
|
chart, release_id, status, manifest_name, chart_name,
|
||||||
result)
|
result)
|
||||||
if isinstance(res, dict):
|
if isinstance(res, dict):
|
||||||
if 'protected' in res:
|
if 'protected' in res:
|
||||||
|
@ -269,22 +249,19 @@ class ChartDeploy(object):
|
||||||
last_test_passed = old_release and r.get_last_test_result(old_release)
|
last_test_passed = old_release and r.get_last_test_result(old_release)
|
||||||
|
|
||||||
test_handler = Test(
|
test_handler = Test(
|
||||||
chart,
|
chart, release_id, self.helm, cg_test_charts=cg_test_all_charts)
|
||||||
release_name,
|
|
||||||
self.tiller,
|
|
||||||
cg_test_charts=cg_test_all_charts)
|
|
||||||
|
|
||||||
run_test = test_handler.test_enabled and (
|
run_test = test_handler.test_enabled and (
|
||||||
just_deployed or not last_test_passed)
|
just_deployed or not last_test_passed)
|
||||||
if run_test:
|
if run_test:
|
||||||
with metrics.CHART_TEST.get_context(test_handler.timeout,
|
with metrics.CHART_TEST.get_context(test_handler.timeout,
|
||||||
manifest_name, chart_name):
|
manifest_name, chart_name):
|
||||||
self._test_chart(release_name, test_handler)
|
self._test_chart(test_handler)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def purge_release(
|
def purge_release(
|
||||||
self, chart, release_name, status, manifest_name, chart_name,
|
self, chart, release_id, status, manifest_name, chart_name,
|
||||||
result):
|
result):
|
||||||
protected = chart.get('protected', {})
|
protected = chart.get('protected', {})
|
||||||
if protected:
|
if protected:
|
||||||
|
@ -293,41 +270,27 @@ class ChartDeploy(object):
|
||||||
LOG.warn(
|
LOG.warn(
|
||||||
'Release %s is `protected`, '
|
'Release %s is `protected`, '
|
||||||
'continue_processing=True. Operator must '
|
'continue_processing=True. Operator must '
|
||||||
'handle %s release manually.', release_name, status)
|
'handle %s release manually.', release_id, status)
|
||||||
result['protected'] = release_name
|
result['protected'] = release_id
|
||||||
return result
|
return result
|
||||||
else:
|
else:
|
||||||
LOG.error(
|
LOG.error(
|
||||||
'Release %s is `protected`, '
|
'Release %s is `protected`, '
|
||||||
'continue_processing=False.', release_name)
|
'continue_processing=False.', release_id)
|
||||||
raise armada_exceptions.ProtectedReleaseException(
|
raise armada_exceptions.ProtectedReleaseException(
|
||||||
release_name, status)
|
release_id, status)
|
||||||
else:
|
else:
|
||||||
# Purge the release
|
# Purge the release
|
||||||
with metrics.CHART_DELETE.get_context(manifest_name, chart_name):
|
with metrics.CHART_DELETE.get_context(manifest_name, chart_name):
|
||||||
|
|
||||||
LOG.info(
|
LOG.info(
|
||||||
'Purging release %s with status %s', release_name, status)
|
'Purging release %s with status %s', release_id, status)
|
||||||
chart_delete = ChartDelete(chart, release_name, self.tiller)
|
chart_delete = ChartDelete(chart, release_id, self.helm)
|
||||||
chart_delete.delete()
|
chart_delete.delete()
|
||||||
result['purge'] = release_name
|
result['purge'] = release_id
|
||||||
|
|
||||||
def _test_chart(self, release_name, test_handler):
|
def _test_chart(self, test_handler):
|
||||||
success = test_handler.test_release_for_success()
|
test_handler.test_release()
|
||||||
if not success:
|
|
||||||
raise tiller_ex.TestFailedException(release_name)
|
|
||||||
|
|
||||||
def get_diff(self, old_chart, old_values, new_chart, values):
|
def get_diff(self, old_chart, old_values, new_chart, values):
|
||||||
return ReleaseDiff(old_chart, old_values, new_chart, values).get_diff()
|
return ReleaseDiff(old_chart, old_values, new_chart, values).get_diff()
|
||||||
|
|
||||||
def find_chart_release(self, known_releases, release_name):
|
|
||||||
'''
|
|
||||||
Find a release given a list of known_releases and a release name
|
|
||||||
'''
|
|
||||||
for release in known_releases:
|
|
||||||
if release.name == release_name:
|
|
||||||
return release
|
|
||||||
LOG.info(
|
|
||||||
"known: %s, release_name: %s",
|
|
||||||
list(map(lambda r: r.name, known_releases)), release_name)
|
|
||||||
return None
|
|
||||||
|
|
|
@ -13,20 +13,14 @@
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
from pathlib import Path
|
||||||
|
from shutil import rmtree
|
||||||
|
|
||||||
from google.protobuf.any_pb2 import Any
|
|
||||||
from hapi.chart.chart_pb2 import Chart
|
|
||||||
from hapi.chart.config_pb2 import Config
|
|
||||||
from hapi.chart.metadata_pb2 import Metadata
|
|
||||||
from hapi.chart.template_pb2 import Template
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
import yaml
|
|
||||||
|
|
||||||
from armada import const
|
from armada import const
|
||||||
from armada.exceptions import chartbuilder_exceptions
|
from armada.exceptions import chartbuilder_exceptions
|
||||||
from armada.handlers.schema import get_schema_info
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -36,11 +30,11 @@ CONF = cfg.CONF
|
||||||
class ChartBuilder(object):
|
class ChartBuilder(object):
|
||||||
'''
|
'''
|
||||||
This class handles taking chart intentions as a parameter and turning those
|
This class handles taking chart intentions as a parameter and turning those
|
||||||
into proper ``protoc`` Helm charts that can be pushed to Tiller.
|
into proper Helm chart metadata.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_chart_doc(cls, chart):
|
def from_chart_doc(cls, chart, helm):
|
||||||
'''
|
'''
|
||||||
Returns a ChartBuilder defined by an Armada Chart doc.
|
Returns a ChartBuilder defined by an Armada Chart doc.
|
||||||
|
|
||||||
|
@ -49,294 +43,71 @@ class ChartBuilder(object):
|
||||||
|
|
||||||
name = chart['metadata']['name']
|
name = chart['metadata']['name']
|
||||||
chart_data = chart[const.KEYWORD_DATA]
|
chart_data = chart[const.KEYWORD_DATA]
|
||||||
source_dir = chart_data.get('source_dir')
|
source_dir = chart_data['source_dir']
|
||||||
source_directory = os.path.join(*source_dir)
|
source_directory = os.path.join(*source_dir)
|
||||||
dependencies = chart_data.get('dependencies')
|
dependencies = chart_data.get('dependencies')
|
||||||
|
|
||||||
# TODO: Remove when v1 doc support is removed.
|
|
||||||
schema_info = get_schema_info(chart['schema'])
|
|
||||||
if schema_info.version < 2:
|
|
||||||
fix_tpl_name = False
|
|
||||||
else:
|
|
||||||
fix_tpl_name = True
|
|
||||||
|
|
||||||
if dependencies is not None:
|
if dependencies is not None:
|
||||||
dependency_builders = []
|
# Ensure `charts` dir exists and is empty.
|
||||||
|
charts_dir = os.path.join(source_directory, 'charts')
|
||||||
|
charts_path = Path(charts_dir)
|
||||||
|
if charts_path.is_dir():
|
||||||
|
# NOTE: Ideally we would only delete the subcharts being
|
||||||
|
# overridden, and leave the others in place, but we delete all
|
||||||
|
# for backward compatibility with the Helm 2 based Armada.
|
||||||
|
(rmtree(d) for d in charts_path.iterdir() if d.is_dir())
|
||||||
|
else:
|
||||||
|
if charts_path.exists():
|
||||||
|
# NOTE: Ideally we would throw an error if `charts` is a
|
||||||
|
# non-directory, but we don't for backward compatibility
|
||||||
|
# with the Helm 2 based Armada.
|
||||||
|
charts_path.unlink()
|
||||||
|
|
||||||
|
charts_path.mkdir()
|
||||||
|
|
||||||
|
# Add symlinks to dependencies into `charts` dir.
|
||||||
for chart_dep in dependencies:
|
for chart_dep in dependencies:
|
||||||
builder = ChartBuilder.from_chart_doc(chart_dep)
|
# Handle any recursive dependencies.
|
||||||
dependency_builders.append(builder)
|
ChartBuilder.from_chart_doc(chart_dep, helm)
|
||||||
|
|
||||||
return cls(
|
dep_data = chart_dep[const.KEYWORD_DATA]
|
||||||
name,
|
dep_source_dir = dep_data['source_dir']
|
||||||
source_directory,
|
dep_source_directory = os.path.join(*dep_source_dir)
|
||||||
dependency_builders,
|
dep_charts_yaml = helm.show_chart(dep_source_directory)
|
||||||
fix_tpl_name=fix_tpl_name)
|
dep_name = dep_charts_yaml['name']
|
||||||
|
dep_target_directory = os.path.join(charts_dir, dep_name)
|
||||||
|
Path(dep_target_directory).symlink_to(dep_source_directory)
|
||||||
|
|
||||||
return cls.from_source(
|
return cls(name, source_directory, helm)
|
||||||
name, source_directory, fix_tpl_name=fix_tpl_name)
|
|
||||||
|
|
||||||
@classmethod
|
def __init__(self, name, source_directory, helm):
|
||||||
def from_source(cls, name, source_directory, fix_tpl_name=False):
|
|
||||||
'''
|
|
||||||
Returns a ChartBuilder, which gets it dependencies from within the Helm
|
|
||||||
chart itself.
|
|
||||||
|
|
||||||
:param name: A name to use for the chart.
|
|
||||||
:param source_directory: The source directory of the Helm chart.
|
|
||||||
'''
|
|
||||||
dependency_builders = []
|
|
||||||
charts_dir = os.path.join(source_directory, 'charts')
|
|
||||||
if os.path.isdir(charts_dir):
|
|
||||||
for f in os.scandir(charts_dir):
|
|
||||||
if not f.is_dir():
|
|
||||||
# TODO: Support ".tgz" dependency charts.
|
|
||||||
|
|
||||||
# Ignore regular files.
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Ignore directories that start with "." or "_".
|
|
||||||
if re.match(r'^[._]', f.name):
|
|
||||||
continue
|
|
||||||
|
|
||||||
builder = ChartBuilder.from_source(
|
|
||||||
f.name, f.path, fix_tpl_name=fix_tpl_name)
|
|
||||||
dependency_builders.append(builder)
|
|
||||||
|
|
||||||
return cls(
|
|
||||||
name,
|
|
||||||
source_directory,
|
|
||||||
dependency_builders,
|
|
||||||
fix_tpl_name=fix_tpl_name)
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self, name, source_directory, dependency_builders,
|
|
||||||
fix_tpl_name=False):
|
|
||||||
'''
|
'''
|
||||||
:param name: A name to use for the chart.
|
:param name: A name to use for the chart.
|
||||||
:param source_directory: The source directory of the Helm chart.
|
:param source_directory: The source directory of the Helm chart.
|
||||||
:param dependency_builders: ChartBuilders to use to build the Helm
|
:param helm: Helm client to calculate the helm chart object.
|
||||||
chart's dependency charts.
|
|
||||||
'''
|
'''
|
||||||
self.name = name
|
self.name = name
|
||||||
self.source_directory = source_directory
|
self.source_directory = source_directory
|
||||||
self.dependency_builders = dependency_builders
|
self.helm = helm
|
||||||
self.fix_tpl_name = fix_tpl_name
|
|
||||||
|
|
||||||
# cache for generated protoc chart object
|
# cache for generated chart object
|
||||||
self._helm_chart = None
|
self._helm_chart = None
|
||||||
|
|
||||||
# load ignored files from .helmignore if present
|
# We do a dry-run upgrade here to get the helm chart metadata.
|
||||||
self.ignored_files = self.get_ignored_files()
|
# Ideally helm would support an explicit machine readable way to
|
||||||
|
# get that data so we don't need the dry run upgrade which could
|
||||||
def get_ignored_files(self):
|
# fail for other reasons than not being able to get the chart
|
||||||
'''Load files to ignore from .helmignore if present.'''
|
# metadata, see:
|
||||||
try:
|
# https://github.com/helm/helm/issues/9968
|
||||||
ignored_files = []
|
def get_helm_chart(self, release_id, values):
|
||||||
if os.path.exists(os.path.join(self.source_directory,
|
|
||||||
'.helmignore')):
|
|
||||||
with open(os.path.join(self.source_directory,
|
|
||||||
'.helmignore')) as f:
|
|
||||||
ignored_files = f.readlines()
|
|
||||||
return [filename.strip() for filename in ignored_files]
|
|
||||||
except Exception:
|
|
||||||
raise chartbuilder_exceptions.IgnoredFilesLoadException()
|
|
||||||
|
|
||||||
def ignore_file(self, filename):
|
|
||||||
'''Returns whether a given ``filename`` should be ignored.
|
|
||||||
|
|
||||||
:param filename: Filename to compare against list of ignored files.
|
|
||||||
:returns: True if file matches an ignored file wildcard or exact name,
|
|
||||||
False otherwise.
|
|
||||||
'''
|
|
||||||
for ignored_file in self.ignored_files:
|
|
||||||
if (ignored_file.startswith('*')
|
|
||||||
and filename.endswith(ignored_file.strip('*'))):
|
|
||||||
return True
|
|
||||||
elif ignored_file == filename:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_metadata(self):
|
|
||||||
'''Extract metadata from Chart.yaml to construct an instance of
|
|
||||||
:class:`hapi.chart.metadata_pb2.Metadata`.
|
|
||||||
'''
|
|
||||||
try:
|
|
||||||
with open(os.path.join(self.source_directory, 'Chart.yaml')) as f:
|
|
||||||
chart_yaml = yaml.safe_load(f.read().encode('utf-8'))
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
raise chartbuilder_exceptions.MetadataLoadException()
|
|
||||||
|
|
||||||
# Construct Metadata object.
|
|
||||||
return Metadata(
|
|
||||||
description=chart_yaml.get('description'),
|
|
||||||
name=chart_yaml.get('name'),
|
|
||||||
version=chart_yaml.get('version'))
|
|
||||||
|
|
||||||
def get_files(self):
|
|
||||||
'''
|
|
||||||
Return (non-template) files in this chart.
|
|
||||||
|
|
||||||
Non-template files include all files *except* Chart.yaml, values.yaml,
|
|
||||||
values.toml, and any file nested under charts/ or templates/. The only
|
|
||||||
exception to this rule is charts/.prov
|
|
||||||
|
|
||||||
The class :class:`google.protobuf.any_pb2.Any` is wrapped around
|
|
||||||
each file as that is what Helm uses.
|
|
||||||
|
|
||||||
For more information, see:
|
|
||||||
https://github.com/kubernetes/helm/blob/fa06dd176dbbc247b40950e38c09f978efecaecc/pkg/chartutil/load.go
|
|
||||||
|
|
||||||
:returns: List of non-template files.
|
|
||||||
:rtype: List[:class:`google.protobuf.any_pb2.Any`]
|
|
||||||
'''
|
|
||||||
|
|
||||||
files_to_ignore = ['Chart.yaml', 'values.yaml', 'values.toml']
|
|
||||||
non_template_files = []
|
|
||||||
|
|
||||||
def _append_file_to_result(root, rel_folder_path, file):
|
|
||||||
abspath = os.path.abspath(os.path.join(root, file))
|
|
||||||
relpath = os.path.join(rel_folder_path, file)
|
|
||||||
|
|
||||||
encodings = ('utf-8', 'latin1')
|
|
||||||
unicode_errors = []
|
|
||||||
|
|
||||||
for encoding in encodings:
|
|
||||||
try:
|
|
||||||
with open(abspath, 'r') as f:
|
|
||||||
file_contents = f.read().encode(encoding)
|
|
||||||
except OSError as e:
|
|
||||||
LOG.debug(
|
|
||||||
'Failed to open and read file %s in the helm '
|
|
||||||
'chart directory.', abspath)
|
|
||||||
raise chartbuilder_exceptions.FilesLoadException(
|
|
||||||
file=abspath, details=e)
|
|
||||||
except UnicodeError as e:
|
|
||||||
LOG.debug(
|
|
||||||
'Attempting to read %s using encoding %s.', abspath,
|
|
||||||
encoding)
|
|
||||||
msg = "(encoding=%s) %s" % (encoding, str(e))
|
|
||||||
unicode_errors.append(msg)
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
if len(unicode_errors) == 2:
|
|
||||||
LOG.debug(
|
|
||||||
'Failed to read file %s in the helm chart directory.'
|
|
||||||
' Ensure that it is encoded using utf-8.', abspath)
|
|
||||||
raise chartbuilder_exceptions.FilesLoadException(
|
|
||||||
file=abspath,
|
|
||||||
clazz=unicode_errors[0].__class__.__name__,
|
|
||||||
details='\n'.join(e for e in unicode_errors))
|
|
||||||
|
|
||||||
non_template_files.append(
|
|
||||||
Any(type_url=relpath, value=file_contents))
|
|
||||||
|
|
||||||
for root, dirs, files in os.walk(self.source_directory):
|
|
||||||
relfolder = os.path.split(root)[-1]
|
|
||||||
rel_folder_path = os.path.relpath(root, self.source_directory)
|
|
||||||
|
|
||||||
if not any(root.startswith(os.path.join(self.source_directory, x))
|
|
||||||
for x in ['templates', 'charts']):
|
|
||||||
for file in files:
|
|
||||||
if (file not in files_to_ignore
|
|
||||||
and file not in non_template_files):
|
|
||||||
_append_file_to_result(root, rel_folder_path, file)
|
|
||||||
elif relfolder == 'charts' and '.prov' in files:
|
|
||||||
_append_file_to_result(root, rel_folder_path, '.prov')
|
|
||||||
|
|
||||||
return non_template_files
|
|
||||||
|
|
||||||
def get_values(self):
|
|
||||||
'''Return the chart's (default) values.'''
|
|
||||||
|
|
||||||
# create config object representing unmarshaled values.yaml
|
|
||||||
if os.path.exists(os.path.join(self.source_directory, 'values.yaml')):
|
|
||||||
with open(os.path.join(self.source_directory, 'values.yaml')) as f:
|
|
||||||
raw_values = f.read()
|
|
||||||
else:
|
|
||||||
LOG.warn(
|
|
||||||
"No values.yaml in %s, using empty values",
|
|
||||||
self.source_directory)
|
|
||||||
raw_values = ''
|
|
||||||
|
|
||||||
return Config(raw=raw_values)
|
|
||||||
|
|
||||||
def get_templates(self):
|
|
||||||
'''Return all the chart templates.
|
|
||||||
|
|
||||||
Process all files in templates/ as a template to attach to the chart,
|
|
||||||
building a :class:`hapi.chart.template_pb2.Template` object.
|
|
||||||
'''
|
|
||||||
chart_name = self.name
|
|
||||||
templates = []
|
|
||||||
tpl_dir = os.path.join(self.source_directory, 'templates')
|
|
||||||
if not os.path.exists(tpl_dir):
|
|
||||||
LOG.warn(
|
|
||||||
"Chart %s has no templates directory. "
|
|
||||||
"No templates will be deployed", chart_name)
|
|
||||||
for root, _, files in os.walk(tpl_dir, topdown=True):
|
|
||||||
for tpl_file in files:
|
|
||||||
tname = os.path.relpath(
|
|
||||||
os.path.join(root, tpl_file),
|
|
||||||
# For v1 compatibility, name template relative to template
|
|
||||||
# dir, for v2 fix the name to be relative to the chart root
|
|
||||||
# to match Helm CLI behavior.
|
|
||||||
self.source_directory if self.fix_tpl_name else tpl_dir)
|
|
||||||
# NOTE: If the template name is fixed (see above), then this
|
|
||||||
# also fixes the path passed here, which could theoretically
|
|
||||||
# affect which files get ignored, though unlikely.
|
|
||||||
if self.ignore_file(tname):
|
|
||||||
LOG.debug('Ignoring file %s', tname)
|
|
||||||
continue
|
|
||||||
|
|
||||||
with open(os.path.join(root, tpl_file)) as f:
|
|
||||||
templates.append(
|
|
||||||
Template(name=tname, data=f.read().encode()))
|
|
||||||
|
|
||||||
return templates
|
|
||||||
|
|
||||||
def get_helm_chart(self):
|
|
||||||
'''Return a Helm chart object.
|
'''Return a Helm chart object.
|
||||||
|
|
||||||
Constructs a :class:`hapi.chart.chart_pb2.Chart` object from the
|
|
||||||
``chart`` intentions, including all dependencies.
|
|
||||||
'''
|
'''
|
||||||
if not self._helm_chart:
|
LOG.debug(
|
||||||
self._helm_chart = self._get_helm_chart()
|
|
||||||
|
|
||||||
return self._helm_chart
|
|
||||||
|
|
||||||
def _get_helm_chart(self):
|
|
||||||
LOG.info(
|
|
||||||
"Building chart %s from path %s", self.name, self.source_directory)
|
"Building chart %s from path %s", self.name, self.source_directory)
|
||||||
dependencies = []
|
|
||||||
for dep_builder in self.dependency_builders:
|
|
||||||
LOG.info(
|
|
||||||
"Building dependency chart %s for chart %s.", dep_builder.name,
|
|
||||||
self.name)
|
|
||||||
try:
|
|
||||||
dependencies.append(dep_builder.get_helm_chart())
|
|
||||||
except Exception:
|
|
||||||
raise chartbuilder_exceptions.DependencyException(self.name)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
helm_chart = Chart(
|
result = self.helm.upgrade_release(
|
||||||
metadata=self.get_metadata(),
|
self.source_directory, release_id, values=values, dry_run=True)
|
||||||
templates=self.get_templates(),
|
return result['chart']
|
||||||
dependencies=dependencies,
|
|
||||||
values=self.get_values(),
|
|
||||||
files=self.get_files())
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise chartbuilder_exceptions.HelmChartBuildException(
|
raise chartbuilder_exceptions.HelmChartBuildException(
|
||||||
self.name, details=e)
|
self.name, details=e)
|
||||||
|
|
||||||
return helm_chart
|
|
||||||
|
|
||||||
def dump(self):
|
|
||||||
'''Dumps a chart object as a serialized string so that we can perform a
|
|
||||||
diff.
|
|
||||||
|
|
||||||
It recurses into dependencies.
|
|
||||||
'''
|
|
||||||
return self.get_helm_chart().SerializeToString()
|
|
||||||
|
|
|
@ -0,0 +1,245 @@
|
||||||
|
# Copyright 2021 The Armada Authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from base64 import b64decode
|
||||||
|
import gzip
|
||||||
|
import io
|
||||||
|
import json as JSON
|
||||||
|
import subprocess # nosec
|
||||||
|
import tempfile
|
||||||
|
from typing import NamedTuple
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log as logging
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from armada.exceptions.helm_exceptions import HelmCommandException
|
||||||
|
from armada.handlers.k8s import K8s
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEFAULT_HELM_TIMEOUT = 300
|
||||||
|
|
||||||
|
STATUS_DEPLOYED = 'deployed'
|
||||||
|
STATUS_FAILED = 'failed'
|
||||||
|
|
||||||
|
|
||||||
|
class Helm(object):
|
||||||
|
'''
|
||||||
|
Helm CLI handler
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, bearer_token=None):
|
||||||
|
self.bearer_token = bearer_token
|
||||||
|
|
||||||
|
# init k8s connectivity
|
||||||
|
self.k8s = K8s(bearer_token=self.bearer_token)
|
||||||
|
|
||||||
|
def _run(self, sub_command, args, json=True, timeout=None):
|
||||||
|
if isinstance(sub_command, str):
|
||||||
|
sub_command = [sub_command]
|
||||||
|
command = ['helm'] + sub_command
|
||||||
|
if json:
|
||||||
|
command = command + ['--output', 'json']
|
||||||
|
command = command + args
|
||||||
|
LOG.info('Running command=%s', command)
|
||||||
|
try:
|
||||||
|
result = subprocess.run( # nosec
|
||||||
|
command, check=True, universal_newlines=True,
|
||||||
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||||
|
timeout=timeout)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
raise HelmCommandException(e)
|
||||||
|
|
||||||
|
if json:
|
||||||
|
return JSON.loads(result.stdout)
|
||||||
|
return result.stdout
|
||||||
|
|
||||||
|
def list_releases(self):
|
||||||
|
return self._run(
|
||||||
|
'ls', ['--all-namespaces', '--all'], timeout=DEFAULT_HELM_TIMEOUT)
|
||||||
|
|
||||||
|
def list_release_ids(self):
|
||||||
|
return [
|
||||||
|
HelmReleaseId(r.namespace, r.name) for r in self.list_releases()
|
||||||
|
]
|
||||||
|
|
||||||
|
def install_release(
|
||||||
|
self,
|
||||||
|
chart,
|
||||||
|
release_id,
|
||||||
|
values=None,
|
||||||
|
wait=False,
|
||||||
|
dry_run=False,
|
||||||
|
timeout=None):
|
||||||
|
timeout = self._check_timeout(wait, timeout)
|
||||||
|
|
||||||
|
args = [
|
||||||
|
release_id.name,
|
||||||
|
chart,
|
||||||
|
'--namespace',
|
||||||
|
release_id.namespace,
|
||||||
|
'--create-namespace',
|
||||||
|
'--timeout',
|
||||||
|
'{}s'.format(timeout),
|
||||||
|
# TODO: (also for upgrade_release) This is for backward
|
||||||
|
# compatibility with helm 2 based Armada. We may want to consider
|
||||||
|
# making this configurable and/or defaulting to enabling.
|
||||||
|
'--disable-openapi-validation'
|
||||||
|
]
|
||||||
|
if wait:
|
||||||
|
args = args + ['--wait']
|
||||||
|
if dry_run:
|
||||||
|
args = args + ['--dry-run']
|
||||||
|
|
||||||
|
with _TempValuesFile(values) as values_file:
|
||||||
|
args = args + ['--values', values_file.file.name]
|
||||||
|
return self._run('install', args, timeout=timeout)
|
||||||
|
|
||||||
|
def upgrade_release(
|
||||||
|
self,
|
||||||
|
chart,
|
||||||
|
release_id,
|
||||||
|
disable_hooks=False,
|
||||||
|
values=None,
|
||||||
|
wait=False,
|
||||||
|
dry_run=False,
|
||||||
|
timeout=None,
|
||||||
|
force=False):
|
||||||
|
timeout = self._check_timeout(wait, timeout)
|
||||||
|
|
||||||
|
args = [
|
||||||
|
release_id.name, chart, '--namespace', release_id.namespace,
|
||||||
|
'--timeout', '{}s'.format(timeout), '--disable-openapi-validation'
|
||||||
|
]
|
||||||
|
if disable_hooks:
|
||||||
|
args = args + ['--no-hooks']
|
||||||
|
if force:
|
||||||
|
args = args + ['--force']
|
||||||
|
if wait:
|
||||||
|
args = args + ['--wait']
|
||||||
|
if dry_run:
|
||||||
|
args = args + ['--dry-run']
|
||||||
|
|
||||||
|
with _TempValuesFile(values) as values_file:
|
||||||
|
args = args + ['--values', values_file.file.name]
|
||||||
|
return self._run('upgrade', args, timeout=timeout)
|
||||||
|
|
||||||
|
def test_release(self, release_id, timeout=DEFAULT_HELM_TIMEOUT):
|
||||||
|
return self._run(
|
||||||
|
'test', [
|
||||||
|
release_id.name, '--namespace', release_id.namespace,
|
||||||
|
'--timeout', '{}s'.format(timeout)
|
||||||
|
],
|
||||||
|
json=False,
|
||||||
|
timeout=timeout)
|
||||||
|
|
||||||
|
def release_status(self, release_id, version=None):
|
||||||
|
args = [release_id.name, '--namespace', release_id.namespace]
|
||||||
|
if version is not None:
|
||||||
|
args = args + ['--version', version]
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self._run('status', args)
|
||||||
|
except HelmCommandException as e:
|
||||||
|
stderr = e.called_process_error.stderr.strip()
|
||||||
|
if 'Error: release: not found' == stderr:
|
||||||
|
return None
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Ideally we could just use `helm status`, but this is missing the
|
||||||
|
# chart metadata which we use for release diffing:
|
||||||
|
# https://github.com/helm/helm/issues/9968
|
||||||
|
#
|
||||||
|
# So instead we access the helm release metadata secret directly
|
||||||
|
# as describe here:
|
||||||
|
# https://gist.github.com/DzeryCZ/c4adf39d4a1a99ae6e594a183628eaee
|
||||||
|
def release_metadata(self, release_id, version=None):
|
||||||
|
if version is None:
|
||||||
|
# determine latest version
|
||||||
|
release = self.release_status(release_id)
|
||||||
|
if release is None:
|
||||||
|
return None
|
||||||
|
version = release['version']
|
||||||
|
secret_name = 'sh.helm.release.v1.{}.v{}'.format(
|
||||||
|
release_id.name, version)
|
||||||
|
secret_namespace = release_id.namespace
|
||||||
|
secret = self.k8s.read_namespaced_secret(secret_name, secret_namespace)
|
||||||
|
raw_data = secret.data['release']
|
||||||
|
k8s_data = b64decode(raw_data)
|
||||||
|
helm_data = b64decode(k8s_data)
|
||||||
|
helm_data_file_handle = io.BytesIO(helm_data)
|
||||||
|
helm_json = gzip.GzipFile(fileobj=helm_data_file_handle).read()
|
||||||
|
return JSON.loads(helm_json)
|
||||||
|
|
||||||
|
def uninstall_release(
|
||||||
|
self,
|
||||||
|
release_id,
|
||||||
|
disable_hooks=False,
|
||||||
|
purge=True,
|
||||||
|
timeout=DEFAULT_HELM_TIMEOUT):
|
||||||
|
|
||||||
|
args = [release_id.name, '--namespace', release_id.namespace]
|
||||||
|
if not purge:
|
||||||
|
args = args + ['--keep-history']
|
||||||
|
if disable_hooks:
|
||||||
|
args = args + ['--no-hooks']
|
||||||
|
return self._run('uninstall', args, json=False, timeout=timeout)
|
||||||
|
|
||||||
|
def show_chart(self, chart_dir):
|
||||||
|
|
||||||
|
output = self._run(['show', 'chart'], [chart_dir], json=False)
|
||||||
|
return yaml.safe_load(output)
|
||||||
|
|
||||||
|
def _check_timeout(self, wait, timeout):
|
||||||
|
if timeout is None or timeout <= 0:
|
||||||
|
timeout = DEFAULT_HELM_TIMEOUT
|
||||||
|
if wait:
|
||||||
|
LOG.warn(
|
||||||
|
'Helm timeout is invalid or unspecified, '
|
||||||
|
'using default %ss.', timeout)
|
||||||
|
return timeout
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, traceback):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class _TempValuesFile():
|
||||||
|
def __init__(self, values):
|
||||||
|
self.values = values
|
||||||
|
self.file = tempfile.NamedTemporaryFile(
|
||||||
|
mode='w', prefix='armada_values', suffix='.yaml')
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self.file.__enter__()
|
||||||
|
values_content = yaml.safe_dump(self.values)
|
||||||
|
self.file.write(values_content)
|
||||||
|
self.file.flush()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, traceback):
|
||||||
|
return self.file.__exit__(exc_type, exc_value, traceback)
|
||||||
|
|
||||||
|
|
||||||
|
class HelmReleaseId(NamedTuple('HelmReleaseId', [('namespace', str),
|
||||||
|
('name', str)])):
|
||||||
|
"""Represents a helm release id."""
|
||||||
|
__slots__ = ()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return '{}/{}'.format(self.namespace, self.name)
|
|
@ -287,6 +287,16 @@ class K8s(object):
|
||||||
return self.apps_v1_api.delete_namespaced_daemon_set(
|
return self.apps_v1_api.delete_namespaced_daemon_set(
|
||||||
name, namespace, body)
|
name, namespace, body)
|
||||||
|
|
||||||
|
def read_namespaced_secret(self, name, namespace="default", **kwargs):
|
||||||
|
'''
|
||||||
|
:param namespace: namespace of the Pod
|
||||||
|
:param label_selector: filters Pods by label
|
||||||
|
|
||||||
|
This will return a list of objects req namespace
|
||||||
|
'''
|
||||||
|
|
||||||
|
return self.client.read_namespaced_secret(name, namespace, **kwargs)
|
||||||
|
|
||||||
def wait_for_pod_redeployment(self, old_pod_name, namespace):
|
def wait_for_pod_redeployment(self, old_pod_name, namespace):
|
||||||
'''
|
'''
|
||||||
:param old_pod_name: name of pods
|
:param old_pod_name: name of pods
|
||||||
|
|
|
@ -23,7 +23,7 @@ from oslo_config import cfg
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
|
|
||||||
from armada.handlers.k8s import K8s
|
from armada.handlers.k8s import K8s
|
||||||
from armada.handlers.tiller import Tiller
|
from armada.handlers.helm import Helm
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
@ -54,17 +54,17 @@ def lock_and_thread(lock_name="lock"):
|
||||||
@functools.wraps(func)
|
@functools.wraps(func)
|
||||||
def func_wrapper(*args, **kwargs):
|
def func_wrapper(*args, **kwargs):
|
||||||
bearer_token = None
|
bearer_token = None
|
||||||
found_tiller = False
|
found_helm = False
|
||||||
for arg in args:
|
for arg in args:
|
||||||
if type(arg) == Tiller:
|
if type(arg) == Helm:
|
||||||
bearer_token = arg.bearer_token
|
bearer_token = arg.bearer_token
|
||||||
found_tiller = True
|
found_helm = True
|
||||||
|
|
||||||
# we did not find a Tiller object to extract a bearer token from
|
# we did not find a Helm object to extract a bearer token from
|
||||||
# log this to assist with potential debugging in the future
|
# log this to assist with potential debugging in the future
|
||||||
if not found_tiller:
|
if not found_helm:
|
||||||
LOG.info(
|
LOG.info(
|
||||||
"no Tiller object found in parameters of function "
|
"no Helm object found in parameters of function "
|
||||||
"decorated by lock_and_thread, this might create "
|
"decorated by lock_and_thread, this might create "
|
||||||
"authentication issues in Kubernetes clusters with "
|
"authentication issues in Kubernetes clusters with "
|
||||||
"external auth backend")
|
"external auth backend")
|
||||||
|
|
|
@ -13,9 +13,6 @@
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from deepdiff import DeepDiff
|
from deepdiff import DeepDiff
|
||||||
import yaml
|
|
||||||
|
|
||||||
from armada.exceptions import armada_exceptions
|
|
||||||
|
|
||||||
|
|
||||||
class ReleaseDiff(object):
|
class ReleaseDiff(object):
|
||||||
|
@ -60,51 +57,10 @@ class ReleaseDiff(object):
|
||||||
:rtype: dict
|
:rtype: dict
|
||||||
'''
|
'''
|
||||||
|
|
||||||
old_input = self.make_release_input(
|
old_input = self.make_release_input(self.old_chart, self.old_values)
|
||||||
self.old_chart, self.old_values, 'previously deployed')
|
new_input = self.make_release_input(self.new_chart, self.new_values)
|
||||||
new_input = self.make_release_input(
|
|
||||||
self.new_chart, self.new_values, 'currently being deployed')
|
|
||||||
|
|
||||||
return DeepDiff(old_input, new_input, view='tree')
|
return DeepDiff(old_input, new_input, view='tree')
|
||||||
|
|
||||||
def make_release_input(self, chart, values, desc):
|
def make_release_input(self, chart, values):
|
||||||
return {'chart': self.make_chart_dict(chart, desc), 'values': values}
|
return {'chart': chart, 'values': values}
|
||||||
|
|
||||||
def make_chart_dict(self, chart, desc):
|
|
||||||
try:
|
|
||||||
default_values = yaml.safe_load(chart.values.raw)
|
|
||||||
except yaml.YAMLError:
|
|
||||||
chart_desc = '{} ({})'.format(chart.metadata.name, desc)
|
|
||||||
raise armada_exceptions.InvalidValuesYamlException(chart_desc)
|
|
||||||
files = {f.type_url: f.value for f in chart.files}
|
|
||||||
|
|
||||||
# With armada/Chart/v1, Armada deployed releases with incorrect
|
|
||||||
# template names, omitting the `templates/` prefix, which is fixed in
|
|
||||||
# v2. This aligns these template names, so that the prefixes match, to
|
|
||||||
# avoid unwanted updates to releases when consuming this fix.
|
|
||||||
def fix_tpl_name(tpl_name):
|
|
||||||
CORRECT_PREFIX = 'templates/'
|
|
||||||
if tpl_name.startswith(CORRECT_PREFIX):
|
|
||||||
return tpl_name
|
|
||||||
return CORRECT_PREFIX + tpl_name
|
|
||||||
|
|
||||||
templates = {fix_tpl_name(t.name): t.data for t in chart.templates}
|
|
||||||
|
|
||||||
dependencies = {
|
|
||||||
d.metadata.name: self.make_chart_dict(
|
|
||||||
d, '{}({} dependency)'.format(desc, d.metadata.name))
|
|
||||||
for d in chart.dependencies
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
# TODO(seaneagan): Are there use cases to include other
|
|
||||||
# `chart.metadata` (Chart.yaml) fields? If so, could include option
|
|
||||||
# under `upgrade` key in armada chart schema for this. Or perhaps
|
|
||||||
# can even add `upgrade.always` there to handle dynamic things
|
|
||||||
# used in charts like dates, environment variables, etc.
|
|
||||||
'name': chart.metadata.name,
|
|
||||||
'values': default_values,
|
|
||||||
'files': files,
|
|
||||||
'templates': templates,
|
|
||||||
'dependencies': dependencies
|
|
||||||
}
|
|
||||||
|
|
|
@ -16,43 +16,36 @@ from oslo_log import log as logging
|
||||||
|
|
||||||
from armada import const
|
from armada import const
|
||||||
from armada.handlers.wait import get_wait_labels
|
from armada.handlers.wait import get_wait_labels
|
||||||
|
from armada.exceptions.helm_exceptions import HelmCommandException
|
||||||
from armada.utils.release import label_selectors
|
from armada.utils.release import label_selectors
|
||||||
from armada.utils.helm import get_test_suite_run_success, is_test_pod
|
from armada.utils.helm import is_test_pod
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Test(object):
|
class Test(object):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self, chart, release_id, helm, cg_test_charts=None,
|
||||||
chart,
|
|
||||||
release_name,
|
|
||||||
tiller,
|
|
||||||
cg_test_charts=None,
|
|
||||||
cleanup=None,
|
|
||||||
enable_all=False):
|
enable_all=False):
|
||||||
"""Initialize a test handler to run Helm tests corresponding to a
|
"""Initialize a test handler to run Helm tests corresponding to a
|
||||||
release.
|
release.
|
||||||
|
|
||||||
:param chart: The armada chart document
|
:param chart: The armada chart document
|
||||||
:param release_name: Name of a Helm release
|
:param release_id: Id of a Helm release
|
||||||
:param tiller: Tiller object
|
:param helm: helm object
|
||||||
:param cg_test_charts: Chart group `test_charts` key
|
:param cg_test_charts: Chart group `test_charts` key
|
||||||
:param cleanup: Triggers cleanup; overrides `test.options.cleanup`
|
|
||||||
:param enable_all: Run tests regardless of the value of `test.enabled`
|
:param enable_all: Run tests regardless of the value of `test.enabled`
|
||||||
|
|
||||||
:type chart: dict
|
:type chart: dict
|
||||||
:type release_name: str
|
:type release_id: HelmReleaseId
|
||||||
:type tiller: Tiller object
|
:type helm: helm object
|
||||||
:type cg_test_charts: bool
|
:type cg_test_charts: bool
|
||||||
:type cleanup: bool
|
|
||||||
:type enable_all: bool
|
:type enable_all: bool
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.chart = chart
|
self.chart = chart
|
||||||
self.release_name = release_name
|
self.release_id = release_id
|
||||||
self.tiller = tiller
|
self.helm = helm
|
||||||
self.cleanup = cleanup
|
|
||||||
self.k8s_timeout = const.DEFAULT_K8S_TIMEOUT
|
self.k8s_timeout = const.DEFAULT_K8S_TIMEOUT
|
||||||
|
|
||||||
test_values = self.chart.get('test', None)
|
test_values = self.chart.get('test', None)
|
||||||
|
@ -76,28 +69,12 @@ class Test(object):
|
||||||
|
|
||||||
self.test_enabled = test_values
|
self.test_enabled = test_values
|
||||||
|
|
||||||
# NOTE: Use old, default cleanup value (i.e. True) if none is
|
|
||||||
# provided.
|
|
||||||
if self.cleanup is None:
|
|
||||||
self.cleanup = True
|
|
||||||
|
|
||||||
elif test_values:
|
elif test_values:
|
||||||
test_enabled_opt = test_values.get('enabled')
|
test_enabled_opt = test_values.get('enabled')
|
||||||
if test_enabled_opt is not None:
|
if test_enabled_opt is not None:
|
||||||
self.test_enabled = test_enabled_opt
|
self.test_enabled = test_enabled_opt
|
||||||
|
|
||||||
# NOTE(drewwalters96): `self.cleanup`, the cleanup value provided
|
|
||||||
# by the API/CLI, takes precedence over the chart value
|
|
||||||
# `test.cleanup`.
|
|
||||||
if self.cleanup is None:
|
|
||||||
test_options = test_values.get('options', {})
|
|
||||||
self.cleanup = test_options.get('cleanup', False)
|
|
||||||
|
|
||||||
self.timeout = test_values.get('timeout', self.timeout)
|
self.timeout = test_values.get('timeout', self.timeout)
|
||||||
else:
|
|
||||||
# Default cleanup value
|
|
||||||
if self.cleanup is None:
|
|
||||||
self.cleanup = False
|
|
||||||
|
|
||||||
if enable_all:
|
if enable_all:
|
||||||
self.test_enabled = True
|
self.test_enabled = True
|
||||||
|
@ -106,10 +83,22 @@ class Test(object):
|
||||||
"""Run the Helm tests corresponding to a release for success (i.e. exit
|
"""Run the Helm tests corresponding to a release for success (i.e. exit
|
||||||
code 0).
|
code 0).
|
||||||
|
|
||||||
|
:return: Helm test suite run result
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.test_release()
|
||||||
|
except HelmCommandException:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def test_release(self):
|
||||||
|
"""Run the Helm tests corresponding to a release for success (i.e. exit
|
||||||
|
code 0).
|
||||||
|
|
||||||
:return: Helm test suite run result
|
:return: Helm test suite run result
|
||||||
"""
|
"""
|
||||||
LOG.info(
|
LOG.info(
|
||||||
'RUNNING: %s tests with timeout=%ds', self.release_name,
|
'RUNNING: %s tests with timeout=%ds', self.release_id,
|
||||||
self.timeout)
|
self.timeout)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -117,18 +106,9 @@ class Test(object):
|
||||||
except Exception:
|
except Exception:
|
||||||
LOG.exception(
|
LOG.exception(
|
||||||
"Exception when deleting test pods for release: %s",
|
"Exception when deleting test pods for release: %s",
|
||||||
self.release_name)
|
self.release_id)
|
||||||
|
|
||||||
test_suite_run = self.tiller.test_release(
|
self.helm.test_release(self.release_id, timeout=self.timeout)
|
||||||
self.release_name, timeout=self.timeout, cleanup=self.cleanup)
|
|
||||||
|
|
||||||
success = get_test_suite_run_success(test_suite_run)
|
|
||||||
if success:
|
|
||||||
LOG.info('PASSED: %s', self.release_name)
|
|
||||||
else:
|
|
||||||
LOG.info('FAILED: %s', self.release_name)
|
|
||||||
|
|
||||||
return success
|
|
||||||
|
|
||||||
def delete_test_pods(self):
|
def delete_test_pods(self):
|
||||||
"""Deletes any existing test pods for the release, as identified by the
|
"""Deletes any existing test pods for the release, as identified by the
|
||||||
|
@ -151,7 +131,7 @@ class Test(object):
|
||||||
'timeout_seconds': self.k8s_timeout
|
'timeout_seconds': self.k8s_timeout
|
||||||
}
|
}
|
||||||
|
|
||||||
pod_list = self.tiller.k8s.client.list_namespaced_pod(**list_args)
|
pod_list = self.helm.k8s.client.list_namespaced_pod(**list_args)
|
||||||
test_pods = [pod for pod in pod_list.items if is_test_pod(pod)]
|
test_pods = [pod for pod in pod_list.items if is_test_pod(pod)]
|
||||||
|
|
||||||
if test_pods:
|
if test_pods:
|
||||||
|
@ -162,5 +142,5 @@ class Test(object):
|
||||||
for test_pod in test_pods:
|
for test_pod in test_pods:
|
||||||
pod_name = test_pod.metadata.name
|
pod_name = test_pod.metadata.name
|
||||||
LOG.info('Deleting existing test pod: %s', pod_name)
|
LOG.info('Deleting existing test pod: %s', pod_name)
|
||||||
self.tiller.k8s.delete_pod_action(
|
self.helm.k8s.delete_pod_action(
|
||||||
pod_name, namespace, timeout=self.k8s_timeout)
|
pod_name, namespace, timeout=self.k8s_timeout)
|
||||||
|
|
|
@ -31,7 +31,6 @@ from armada import const
|
||||||
from armada.exceptions import tiller_exceptions as ex
|
from armada.exceptions import tiller_exceptions as ex
|
||||||
from armada.handlers.k8s import K8s
|
from armada.handlers.k8s import K8s
|
||||||
from armada.utils import helm
|
from armada.utils import helm
|
||||||
from armada.utils.release import get_release_status
|
|
||||||
|
|
||||||
TILLER_VERSION = b'2.16.9'
|
TILLER_VERSION = b'2.16.9'
|
||||||
GRPC_EPSILON = 60
|
GRPC_EPSILON = 60
|
||||||
|
@ -268,7 +267,7 @@ class Tiller(object):
|
||||||
if latest_versions[r.name] == r.version:
|
if latest_versions[r.name] == r.version:
|
||||||
LOG.debug(
|
LOG.debug(
|
||||||
'Found release %s, version %s, status: %s', r.name,
|
'Found release %s, version %s, status: %s', r.name,
|
||||||
r.version, get_release_status(r))
|
r.version, r.info.status)
|
||||||
latest_releases.append(r)
|
latest_releases.append(r)
|
||||||
|
|
||||||
return latest_releases
|
return latest_releases
|
||||||
|
|
|
@ -46,15 +46,14 @@ def get_wait_labels(chart):
|
||||||
# TODO: Validate this object up front in armada validate flow.
|
# TODO: Validate this object up front in armada validate flow.
|
||||||
class ChartWait():
|
class ChartWait():
|
||||||
def __init__(
|
def __init__(
|
||||||
self, k8s, release_name, chart, namespace, k8s_wait_attempts,
|
self, k8s, release_id, chart, k8s_wait_attempts,
|
||||||
k8s_wait_attempt_sleep, timeout):
|
k8s_wait_attempt_sleep, timeout):
|
||||||
self.k8s = k8s
|
self.k8s = k8s
|
||||||
self.release_name = release_name
|
self.release_id = release_id
|
||||||
self.chart = chart
|
self.chart = chart
|
||||||
chart_data = self.chart[const.KEYWORD_DATA]
|
chart_data = self.chart[const.KEYWORD_DATA]
|
||||||
self.chart_data = chart_data
|
self.chart_data = chart_data
|
||||||
self.wait_config = self.chart_data.get('wait', {})
|
self.wait_config = self.chart_data.get('wait', {})
|
||||||
self.namespace = namespace
|
|
||||||
self.k8s_wait_attempts = max(k8s_wait_attempts, 1)
|
self.k8s_wait_attempts = max(k8s_wait_attempts, 1)
|
||||||
self.k8s_wait_attempt_sleep = max(k8s_wait_attempt_sleep, 1)
|
self.k8s_wait_attempt_sleep = max(k8s_wait_attempt_sleep, 1)
|
||||||
|
|
||||||
|
@ -276,8 +275,8 @@ class ResourceWait(ABC):
|
||||||
LOG.info(
|
LOG.info(
|
||||||
"Waiting for resource type=%s, namespace=%s labels=%s "
|
"Waiting for resource type=%s, namespace=%s labels=%s "
|
||||||
"required=%s%s for %ss", self.resource_type,
|
"required=%s%s for %ss", self.resource_type,
|
||||||
self.chart_wait.namespace, self.label_selector, self.required,
|
self.chart_wait.release_id.namespace, self.label_selector,
|
||||||
min_ready_msg, timeout)
|
self.required, min_ready_msg, timeout)
|
||||||
if not self.label_selector:
|
if not self.label_selector:
|
||||||
LOG.warn(
|
LOG.warn(
|
||||||
'"label_selector" not specified, waiting with no labels '
|
'"label_selector" not specified, waiting with no labels '
|
||||||
|
@ -336,7 +335,7 @@ class ResourceWait(ABC):
|
||||||
error = (
|
error = (
|
||||||
"Timed out waiting for resource type={}, namespace={}, "
|
"Timed out waiting for resource type={}, namespace={}, "
|
||||||
"labels={}".format(
|
"labels={}".format(
|
||||||
self.resource_type, self.chart_wait.namespace,
|
self.resource_type, self.chart_wait.release_id.namespace,
|
||||||
self.label_selector))
|
self.label_selector))
|
||||||
LOG.error(error)
|
LOG.error(error)
|
||||||
raise k8s_exceptions.KubernetesWatchTimeoutException(error)
|
raise k8s_exceptions.KubernetesWatchTimeoutException(error)
|
||||||
|
@ -360,7 +359,7 @@ class ResourceWait(ABC):
|
||||||
error = (
|
error = (
|
||||||
'Timed out waiting for {}s (namespace={}, labels=({})). {}'.
|
'Timed out waiting for {}s (namespace={}, labels=({})). {}'.
|
||||||
format(
|
format(
|
||||||
self.resource_type, self.chart_wait.namespace,
|
self.resource_type, self.chart_wait.release_id.namespace,
|
||||||
self.label_selector, details))
|
self.label_selector, details))
|
||||||
LOG.error(error)
|
LOG.error(error)
|
||||||
raise k8s_exceptions.KubernetesWatchTimeoutException(error)
|
raise k8s_exceptions.KubernetesWatchTimeoutException(error)
|
||||||
|
@ -375,14 +374,15 @@ class ResourceWait(ABC):
|
||||||
'''
|
'''
|
||||||
LOG.debug(
|
LOG.debug(
|
||||||
'Starting to wait on: namespace=%s, resource type=%s, '
|
'Starting to wait on: namespace=%s, resource type=%s, '
|
||||||
'label_selector=(%s), timeout=%s', self.chart_wait.namespace,
|
'label_selector=(%s), timeout=%s',
|
||||||
self.resource_type, self.label_selector, timeout)
|
self.chart_wait.release_id.namespace, self.resource_type,
|
||||||
|
self.label_selector, timeout)
|
||||||
ready = {}
|
ready = {}
|
||||||
modified = set()
|
modified = set()
|
||||||
found_resources = False
|
found_resources = False
|
||||||
|
|
||||||
kwargs = {
|
kwargs = {
|
||||||
'namespace': self.chart_wait.namespace,
|
'namespace': self.chart_wait.release_id.namespace,
|
||||||
'label_selector': self.label_selector,
|
'label_selector': self.label_selector,
|
||||||
'timeout_seconds': timeout
|
'timeout_seconds': timeout
|
||||||
}
|
}
|
||||||
|
@ -420,8 +420,8 @@ class ResourceWait(ABC):
|
||||||
'Watch event: type=%s, name=%s, namespace=%s, '
|
'Watch event: type=%s, name=%s, namespace=%s, '
|
||||||
'resource_version=%s')
|
'resource_version=%s')
|
||||||
LOG.debug(
|
LOG.debug(
|
||||||
msg, event_type, resource_name, self.chart_wait.namespace,
|
msg, event_type, resource_name,
|
||||||
resource_version)
|
self.chart_wait.release_id.namespace, resource_version)
|
||||||
|
|
||||||
if event_type in {'ADDED', 'MODIFIED'}:
|
if event_type in {'ADDED', 'MODIFIED'}:
|
||||||
found_resources = True
|
found_resources = True
|
||||||
|
@ -491,9 +491,6 @@ class PodWait(ResourceWait):
|
||||||
return 'owned by job'
|
return 'owned by job'
|
||||||
else:
|
else:
|
||||||
# Exclude all pods with an owner (only include raw pods)
|
# Exclude all pods with an owner (only include raw pods)
|
||||||
# TODO: In helm 3, all resources will likely have the release CR as
|
|
||||||
# an owner, so this will need to be updated to not exclude pods
|
|
||||||
# directly owned by the release.
|
|
||||||
if has_owner(pod):
|
if has_owner(pod):
|
||||||
return 'owned by another resource'
|
return 'owned by another resource'
|
||||||
|
|
||||||
|
|
|
@ -162,8 +162,6 @@ data:
|
||||||
properties:
|
properties:
|
||||||
force:
|
force:
|
||||||
type: boolean
|
type: boolean
|
||||||
recreate_pods:
|
|
||||||
type: boolean
|
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
required:
|
required:
|
||||||
- no_hooks
|
- no_hooks
|
||||||
|
|
|
@ -149,8 +149,6 @@ data:
|
||||||
properties:
|
properties:
|
||||||
force:
|
force:
|
||||||
type: boolean
|
type: boolean
|
||||||
recreate_pods:
|
|
||||||
type: boolean
|
|
||||||
no_hooks:
|
no_hooks:
|
||||||
type: boolean
|
type: boolean
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
|
|
@ -29,11 +29,11 @@ CONF = cfg.CONF
|
||||||
@mock.patch.object(
|
@mock.patch.object(
|
||||||
armada_api.Apply, 'handle', armada_api.Apply.handle.__wrapped__)
|
armada_api.Apply, 'handle', armada_api.Apply.handle.__wrapped__)
|
||||||
class ArmadaControllerTest(base.BaseControllerTest):
|
class ArmadaControllerTest(base.BaseControllerTest):
|
||||||
@mock.patch.object(api, 'Tiller')
|
@mock.patch.object(api, 'Helm')
|
||||||
@mock.patch.object(armada_api, 'Armada')
|
@mock.patch.object(armada_api, 'Armada')
|
||||||
@mock.patch.object(armada_api, 'ReferenceResolver')
|
@mock.patch.object(armada_api, 'ReferenceResolver')
|
||||||
def test_armada_apply_resource(
|
def test_armada_apply_resource(
|
||||||
self, mock_resolver, mock_armada, mock_tiller):
|
self, mock_resolver, mock_armada, mock_helm):
|
||||||
"""Tests the POST /api/v1.0/apply endpoint."""
|
"""Tests the POST /api/v1.0/apply endpoint."""
|
||||||
rules = {'armada:create_endpoints': '@'}
|
rules = {'armada:create_endpoints': '@'}
|
||||||
self.policy.set_rules(rules)
|
self.policy.set_rules(rules)
|
||||||
|
@ -48,8 +48,8 @@ class ArmadaControllerTest(base.BaseControllerTest):
|
||||||
'timeout': '100'
|
'timeout': '100'
|
||||||
}
|
}
|
||||||
|
|
||||||
m_tiller = mock_tiller.return_value
|
m_helm = mock_helm.return_value
|
||||||
m_tiller.__enter__.return_value = m_tiller
|
m_helm.__enter__.return_value = m_helm
|
||||||
|
|
||||||
expected_armada_options = {
|
expected_armada_options = {
|
||||||
'disable_update_pre': False,
|
'disable_update_pre': False,
|
||||||
|
@ -57,7 +57,7 @@ class ArmadaControllerTest(base.BaseControllerTest):
|
||||||
'enable_chart_cleanup': False,
|
'enable_chart_cleanup': False,
|
||||||
'force_wait': False,
|
'force_wait': False,
|
||||||
'timeout': 100,
|
'timeout': 100,
|
||||||
'tiller': m_tiller,
|
'helm': m_helm,
|
||||||
'target_manifest': None
|
'target_manifest': None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,8 +87,8 @@ class ArmadaControllerTest(base.BaseControllerTest):
|
||||||
}], **expected_armada_options)
|
}], **expected_armada_options)
|
||||||
mock_armada.return_value.sync.assert_called()
|
mock_armada.return_value.sync.assert_called()
|
||||||
|
|
||||||
mock_tiller.assert_called()
|
mock_helm.assert_called()
|
||||||
m_tiller.__exit__.assert_called()
|
m_helm.__exit__.assert_called()
|
||||||
|
|
||||||
def test_armada_apply_no_href(self):
|
def test_armada_apply_no_href(self):
|
||||||
"""Tests /api/v1.0/apply returns 400 when hrefs list is empty."""
|
"""Tests /api/v1.0/apply returns 400 when hrefs list is empty."""
|
||||||
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
# Copyright 2017 AT&T Intellectual Property. All other rights reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the 'License');
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an 'AS IS' BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import mock
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
from armada import api
|
||||||
|
from armada.common.policies import base as policy_base
|
||||||
|
from armada.tests import test_utils
|
||||||
|
from armada.tests.unit.api import base
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
|
class ReleasesControllerTest(base.BaseControllerTest):
|
||||||
|
@mock.patch.object(api, 'Helm')
|
||||||
|
def test_helm_releases(self, mock_helm):
|
||||||
|
"""Tests GET /api/v1.0/releases endpoint."""
|
||||||
|
rules = {'armada:get_release': '@'}
|
||||||
|
self.policy.set_rules(rules)
|
||||||
|
|
||||||
|
def _get_fake_release(name, namespace):
|
||||||
|
fake_release = mock.Mock(namespace='%s_namespace' % namespace)
|
||||||
|
fake_release.configure_mock(name=name)
|
||||||
|
return fake_release
|
||||||
|
|
||||||
|
m_helm = mock_helm.return_value
|
||||||
|
m_helm.__enter__.return_value = m_helm
|
||||||
|
m_helm.list_release_ids.return_value = [
|
||||||
|
_get_fake_release('foo', 'bar'),
|
||||||
|
_get_fake_release('baz', 'qux')
|
||||||
|
]
|
||||||
|
|
||||||
|
result = self.app.simulate_get('/api/v1.0/releases')
|
||||||
|
expected = {
|
||||||
|
'releases': {
|
||||||
|
'bar_namespace': ['foo'],
|
||||||
|
'qux_namespace': ['baz']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.assertEqual(expected, result.json)
|
||||||
|
mock_helm.assert_called_once()
|
||||||
|
m_helm.list_release_ids.assert_called_once_with()
|
||||||
|
m_helm.__exit__.assert_called()
|
||||||
|
|
||||||
|
@mock.patch.object(api, 'Helm')
|
||||||
|
def test_helm_releases_with_params(self, mock_helm):
|
||||||
|
"""Tests GET /api/v1.0/releases endpoint with query parameters."""
|
||||||
|
rules = {'armada:get_release': '@'}
|
||||||
|
self.policy.set_rules(rules)
|
||||||
|
|
||||||
|
def _get_fake_release(name, namespace):
|
||||||
|
fake_release = mock.Mock(namespace='%s_namespace' % namespace)
|
||||||
|
fake_release.configure_mock(name=name)
|
||||||
|
return fake_release
|
||||||
|
|
||||||
|
m_helm = mock_helm.return_value
|
||||||
|
m_helm.__enter__.return_value = m_helm
|
||||||
|
m_helm.list_release_ids.return_value = [
|
||||||
|
_get_fake_release('foo', 'bar'),
|
||||||
|
_get_fake_release('baz', 'qux')
|
||||||
|
]
|
||||||
|
|
||||||
|
result = self.app.simulate_get(
|
||||||
|
'/api/v1.0/releases', params_csv=False, params={})
|
||||||
|
expected = {
|
||||||
|
'releases': {
|
||||||
|
'bar_namespace': ['foo'],
|
||||||
|
'qux_namespace': ['baz']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.assertEqual(expected, result.json)
|
||||||
|
mock_helm.assert_called_once()
|
||||||
|
m_helm.list_release_ids.assert_called_once_with()
|
||||||
|
m_helm.__exit__.assert_called()
|
||||||
|
|
||||||
|
|
||||||
|
class ReleasesControllerNegativeRbacTest(base.BaseControllerTest):
|
||||||
|
@test_utils.attr(type=['negative'])
|
||||||
|
def test_list_helm_releases_insufficient_permissions(self):
|
||||||
|
"""Tests the GET /api/v1.0/releases endpoint returns 403 following
|
||||||
|
failed authorization.
|
||||||
|
"""
|
||||||
|
rules = {'armada:get_release': policy_base.RULE_ADMIN_REQUIRED}
|
||||||
|
self.policy.set_rules(rules)
|
||||||
|
resp = self.app.simulate_get('/api/v1.0/releases')
|
||||||
|
self.assertEqual(403, resp.status_code)
|
|
@ -31,21 +31,20 @@ from armada.tests.unit.api import base
|
||||||
test.TestReleasesManifestController.handle.__wrapped__)
|
test.TestReleasesManifestController.handle.__wrapped__)
|
||||||
class TestReleasesManifestControllerTest(base.BaseControllerTest):
|
class TestReleasesManifestControllerTest(base.BaseControllerTest):
|
||||||
@mock.patch.object(test, 'Manifest')
|
@mock.patch.object(test, 'Manifest')
|
||||||
@mock.patch.object(api, 'Tiller')
|
@mock.patch.object(api, 'Helm')
|
||||||
def test_test_controller_with_manifest(self, mock_tiller, mock_manifest):
|
def test_test_controller_with_manifest(self, mock_Helm, mock_manifest):
|
||||||
rules = {'armada:test_manifest': '@'}
|
rules = {'armada:test_manifest': '@'}
|
||||||
self.policy.set_rules(rules)
|
self.policy.set_rules(rules)
|
||||||
|
|
||||||
# TODO: Don't use example charts in tests.
|
# TODO: Don't use example charts in tests.
|
||||||
# TODO: Test cleanup arg is taken from url, then manifest.
|
|
||||||
manifest_path = os.path.join(
|
manifest_path = os.path.join(
|
||||||
os.getcwd(), 'examples', 'keystone-manifest.yaml')
|
os.getcwd(), 'examples', 'keystone-manifest.yaml')
|
||||||
with open(manifest_path, 'r') as f:
|
with open(manifest_path, 'r') as f:
|
||||||
payload = f.read()
|
payload = f.read()
|
||||||
documents = list(yaml.safe_load_all(payload))
|
documents = list(yaml.safe_load_all(payload))
|
||||||
|
|
||||||
m_tiller = mock_tiller.return_value
|
m_helm = mock_Helm.return_value
|
||||||
m_tiller.__enter__.return_value = m_tiller
|
m_helm.__enter__.return_value = m_helm
|
||||||
|
|
||||||
resp = self.app.simulate_post('/api/v1.0/tests', body=payload)
|
resp = self.app.simulate_post('/api/v1.0/tests', body=payload)
|
||||||
self.assertEqual(200, resp.status_code)
|
self.assertEqual(200, resp.status_code)
|
||||||
|
@ -55,8 +54,8 @@ class TestReleasesManifestControllerTest(base.BaseControllerTest):
|
||||||
self.assertEqual(expected, result)
|
self.assertEqual(expected, result)
|
||||||
|
|
||||||
mock_manifest.assert_called_once_with(documents, target_manifest=None)
|
mock_manifest.assert_called_once_with(documents, target_manifest=None)
|
||||||
mock_tiller.assert_called()
|
mock_Helm.assert_called()
|
||||||
m_tiller.__exit__.assert_called()
|
m_helm.__exit__.assert_called()
|
||||||
|
|
||||||
|
|
||||||
@mock.patch.object(
|
@mock.patch.object(
|
||||||
|
@ -64,65 +63,48 @@ class TestReleasesManifestControllerTest(base.BaseControllerTest):
|
||||||
test.TestReleasesReleaseNameController.handle.__wrapped__)
|
test.TestReleasesReleaseNameController.handle.__wrapped__)
|
||||||
class TestReleasesReleaseNameControllerTest(base.BaseControllerTest):
|
class TestReleasesReleaseNameControllerTest(base.BaseControllerTest):
|
||||||
@mock.patch.object(test.Test, 'test_release_for_success')
|
@mock.patch.object(test.Test, 'test_release_for_success')
|
||||||
@mock.patch.object(api, 'Tiller')
|
@mock.patch.object(api, 'Helm')
|
||||||
def test_test_controller_test_pass(
|
def test_test_controller_test_pass(
|
||||||
self, mock_tiller, mock_test_release_for_success):
|
self, mock_Helm, mock_test_release_for_success):
|
||||||
rules = {'armada:test_release': '@'}
|
rules = {'armada:test_release': '@'}
|
||||||
self.policy.set_rules(rules)
|
self.policy.set_rules(rules)
|
||||||
|
|
||||||
mock_test_release_for_success.return_value = True
|
mock_test_release_for_success.return_value = True
|
||||||
|
|
||||||
m_tiller = mock_tiller.return_value
|
m_helm = mock_Helm.return_value
|
||||||
m_tiller.__enter__.return_value = m_tiller
|
m_helm.__enter__.return_value = m_helm
|
||||||
|
|
||||||
|
namespace = 'fake-namespace'
|
||||||
release = 'fake-release'
|
release = 'fake-release'
|
||||||
resp = self.app.simulate_get('/api/v1.0/test/{}'.format(release))
|
resp = self.app.simulate_get(
|
||||||
|
'/api/v1.0/test/{}/{}'.format(namespace, release))
|
||||||
mock_test_release_for_success.assert_called_once()
|
mock_test_release_for_success.assert_called_once()
|
||||||
self.assertEqual(200, resp.status_code)
|
self.assertEqual(200, resp.status_code)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
'MESSAGE: Test Pass',
|
'MESSAGE: Test Pass',
|
||||||
json.loads(resp.text)['message'])
|
json.loads(resp.text)['message'])
|
||||||
m_tiller.__exit__.assert_called()
|
m_helm.__exit__.assert_called()
|
||||||
|
|
||||||
@mock.patch.object(test.Test, 'test_release_for_success')
|
@mock.patch.object(test.Test, 'test_release_for_success')
|
||||||
@mock.patch.object(api, 'Tiller')
|
@mock.patch.object(api, 'Helm')
|
||||||
def test_test_controller_test_fail(
|
def test_test_controller_test_fail(
|
||||||
self, mock_tiller, mock_test_release_for_success):
|
self, mock_Helm, mock_test_release_for_success):
|
||||||
rules = {'armada:test_release': '@'}
|
rules = {'armada:test_release': '@'}
|
||||||
self.policy.set_rules(rules)
|
self.policy.set_rules(rules)
|
||||||
|
|
||||||
m_tiller = mock_tiller.return_value
|
m_helm = mock_Helm.return_value
|
||||||
m_tiller.__enter__.return_value = m_tiller
|
m_helm.__enter__.return_value = m_helm
|
||||||
|
|
||||||
mock_test_release_for_success.return_value = False
|
mock_test_release_for_success.return_value = False
|
||||||
|
namespace = 'fake-namespace'
|
||||||
release = 'fake-release'
|
release = 'fake-release'
|
||||||
resp = self.app.simulate_get('/api/v1.0/test/{}'.format(release))
|
resp = self.app.simulate_get(
|
||||||
|
'/api/v1.0/test/{}/{}'.format(namespace, release))
|
||||||
self.assertEqual(200, resp.status_code)
|
self.assertEqual(200, resp.status_code)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
'MESSAGE: Test Fail',
|
'MESSAGE: Test Fail',
|
||||||
json.loads(resp.text)['message'])
|
json.loads(resp.text)['message'])
|
||||||
m_tiller.__exit__.assert_called()
|
m_helm.__exit__.assert_called()
|
||||||
|
|
||||||
@mock.patch.object(test.Test, 'test_release_for_success')
|
|
||||||
@mock.patch.object(api, 'Tiller')
|
|
||||||
def test_test_controller_cleanup(
|
|
||||||
self, mock_tiller, mock_test_release_for_success):
|
|
||||||
rules = {'armada:test_release': '@'}
|
|
||||||
self.policy.set_rules(rules)
|
|
||||||
|
|
||||||
m_tiller = mock_tiller.return_value
|
|
||||||
m_tiller.__enter__.return_value = m_tiller
|
|
||||||
|
|
||||||
mock_test_release_for_success.return_value = True
|
|
||||||
release = 'fake-release'
|
|
||||||
resp = self.app.simulate_get(
|
|
||||||
'/api/v1.0/test/{}'.format(release), query_string='cleanup=true')
|
|
||||||
mock_test_release_for_success.assert_called_once()
|
|
||||||
self.assertEqual(200, resp.status_code)
|
|
||||||
self.assertEqual(
|
|
||||||
'MESSAGE: Test Pass',
|
|
||||||
json.loads(resp.text)['message'])
|
|
||||||
m_tiller.__exit__.assert_called()
|
|
||||||
|
|
||||||
|
|
||||||
@test_utils.attr(type=['negative'])
|
@test_utils.attr(type=['negative'])
|
||||||
|
@ -131,23 +113,23 @@ class TestReleasesReleaseNameControllerTest(base.BaseControllerTest):
|
||||||
test.TestReleasesManifestController.handle.__wrapped__)
|
test.TestReleasesManifestController.handle.__wrapped__)
|
||||||
class TestReleasesManifestControllerNegativeTest(base.BaseControllerTest):
|
class TestReleasesManifestControllerNegativeTest(base.BaseControllerTest):
|
||||||
@mock.patch.object(test, 'Manifest')
|
@mock.patch.object(test, 'Manifest')
|
||||||
@mock.patch.object(api, 'Tiller')
|
@mock.patch.object(api, 'Helm')
|
||||||
@mock.patch.object(test.Test, 'test_release_for_success')
|
@mock.patch.object(test.Test, 'test_release_for_success')
|
||||||
def test_test_controller_tiller_exc_returns_500(
|
def test_test_controller_Helm_exc_returns_500(
|
||||||
self, mock_test_release_for_success, mock_tiller, _):
|
self, mock_test_release_for_success, mock_Helm, _):
|
||||||
rules = {'armada:test_manifest': '@'}
|
rules = {'armada:test_manifest': '@'}
|
||||||
self.policy.set_rules(rules)
|
self.policy.set_rules(rules)
|
||||||
|
|
||||||
mock_tiller.side_effect = Exception
|
mock_Helm.side_effect = Exception
|
||||||
mock_test_release_for_success.side_effect = Exception
|
mock_test_release_for_success.side_effect = Exception
|
||||||
|
|
||||||
resp = self.app.simulate_post('/api/v1.0/tests')
|
resp = self.app.simulate_post('/api/v1.0/tests')
|
||||||
self.assertEqual(500, resp.status_code)
|
self.assertEqual(500, resp.status_code)
|
||||||
|
|
||||||
@mock.patch.object(test, 'Manifest')
|
@mock.patch.object(test, 'Manifest')
|
||||||
@mock.patch.object(api, 'Tiller')
|
@mock.patch.object(api, 'Helm')
|
||||||
def test_test_controller_validation_failure_returns_400(
|
def test_test_controller_validation_failure_returns_400(
|
||||||
self, mock_tiller, mock_manifest):
|
self, mock_Helm, mock_manifest):
|
||||||
rules = {'armada:test_manifest': '@'}
|
rules = {'armada:test_manifest': '@'}
|
||||||
self.policy.set_rules(rules)
|
self.policy.set_rules(rules)
|
||||||
|
|
||||||
|
@ -163,8 +145,8 @@ class TestReleasesManifestControllerNegativeTest(base.BaseControllerTest):
|
||||||
resp = self.app.simulate_post('/api/v1.0/tests', body=invalid_payload)
|
resp = self.app.simulate_post('/api/v1.0/tests', body=invalid_payload)
|
||||||
self.assertEqual(400, resp.status_code)
|
self.assertEqual(400, resp.status_code)
|
||||||
|
|
||||||
m_tiller = mock_tiller.return_value
|
m_helm = mock_Helm.return_value
|
||||||
m_tiller.__enter__.return_value = m_tiller
|
m_helm.__enter__.return_value = m_helm
|
||||||
|
|
||||||
resp_body = json.loads(resp.text)
|
resp_body = json.loads(resp.text)
|
||||||
self.assertEqual(400, resp_body['code'])
|
self.assertEqual(400, resp_body['code'])
|
||||||
|
@ -185,12 +167,12 @@ class TestReleasesManifestControllerNegativeTest(base.BaseControllerTest):
|
||||||
(
|
(
|
||||||
'Failed to validate documents or generate Armada '
|
'Failed to validate documents or generate Armada '
|
||||||
'Manifest from documents.'), resp_body['message'])
|
'Manifest from documents.'), resp_body['message'])
|
||||||
m_tiller.__exit__.assert_called()
|
m_helm.__exit__.assert_called()
|
||||||
|
|
||||||
@mock.patch('armada.utils.validate.Manifest')
|
@mock.patch('armada.utils.validate.Manifest')
|
||||||
@mock.patch.object(api, 'Tiller')
|
@mock.patch.object(api, 'Helm')
|
||||||
def test_test_controller_manifest_failure_returns_400(
|
def test_test_controller_manifest_failure_returns_400(
|
||||||
self, mock_tiller, mock_manifest):
|
self, mock_Helm, mock_manifest):
|
||||||
rules = {'armada:test_manifest': '@'}
|
rules = {'armada:test_manifest': '@'}
|
||||||
self.policy.set_rules(rules)
|
self.policy.set_rules(rules)
|
||||||
|
|
||||||
|
@ -205,8 +187,8 @@ class TestReleasesManifestControllerNegativeTest(base.BaseControllerTest):
|
||||||
resp = self.app.simulate_post('/api/v1.0/tests', body=payload)
|
resp = self.app.simulate_post('/api/v1.0/tests', body=payload)
|
||||||
self.assertEqual(400, resp.status_code)
|
self.assertEqual(400, resp.status_code)
|
||||||
|
|
||||||
m_tiller = mock_tiller.return_value
|
m_helm = mock_Helm.return_value
|
||||||
m_tiller.__enter__.return_value = m_tiller
|
m_helm.__enter__.return_value = m_helm
|
||||||
|
|
||||||
resp_body = json.loads(resp.text)
|
resp_body = json.loads(resp.text)
|
||||||
self.assertEqual(400, resp_body['code'])
|
self.assertEqual(400, resp_body['code'])
|
||||||
|
@ -228,22 +210,25 @@ class TestReleasesManifestControllerNegativeTest(base.BaseControllerTest):
|
||||||
(
|
(
|
||||||
'Failed to validate documents or generate Armada '
|
'Failed to validate documents or generate Armada '
|
||||||
'Manifest from documents.'), resp_body['message'])
|
'Manifest from documents.'), resp_body['message'])
|
||||||
m_tiller.__exit__.assert_called()
|
m_helm.__exit__.assert_called()
|
||||||
|
|
||||||
|
|
||||||
@test_utils.attr(type=['negative'])
|
@test_utils.attr(type=['negative'])
|
||||||
class TestReleasesReleaseNameControllerNegativeTest(base.BaseControllerTest):
|
class TestReleasesReleaseNameControllerNegativeTest(base.BaseControllerTest):
|
||||||
@mock.patch.object(api, 'Tiller')
|
@mock.patch.object(api, 'Helm')
|
||||||
@mock.patch.object(test.Test, 'test_release_for_success')
|
@mock.patch.object(test.Test, 'test_release_for_success')
|
||||||
def test_test_controller_tiller_exc_returns_500(
|
def test_test_controller_Helm_exc_returns_500(
|
||||||
self, mock_test_release_for_success, mock_tiller):
|
self, mock_test_release_for_success, mock_Helm):
|
||||||
rules = {'armada:test_release': '@'}
|
rules = {'armada:test_release': '@'}
|
||||||
self.policy.set_rules(rules)
|
self.policy.set_rules(rules)
|
||||||
|
|
||||||
mock_tiller.side_effect = Exception
|
mock_Helm.side_effect = Exception
|
||||||
mock_test_release_for_success.side_effect = Exception
|
mock_test_release_for_success.side_effect = Exception
|
||||||
|
|
||||||
resp = self.app.simulate_get('/api/v1.0/test/fake-release')
|
namespace = 'fake-namespace'
|
||||||
|
release = 'fake-release'
|
||||||
|
resp = self.app.simulate_get(
|
||||||
|
'/api/v1.0/test/{}/{}'.format(namespace, release))
|
||||||
self.assertEqual(500, resp.status_code)
|
self.assertEqual(500, resp.status_code)
|
||||||
|
|
||||||
|
|
||||||
|
@ -251,12 +236,13 @@ class TestReleasesReleaseNameControllerNegativeRbacTest(base.BaseControllerTest
|
||||||
):
|
):
|
||||||
@test_utils.attr(type=['negative'])
|
@test_utils.attr(type=['negative'])
|
||||||
def test_test_release_insufficient_permissions(self):
|
def test_test_release_insufficient_permissions(self):
|
||||||
"""Tests the GET /api/v1.0/test/{release} endpoint returns 403
|
"""Tests the GET /api/v1.0/test/{namespace}/{release} endpoint returns 403
|
||||||
following failed authorization.
|
following failed authorization.
|
||||||
"""
|
"""
|
||||||
rules = {'armada:test_release': policy_base.RULE_ADMIN_REQUIRED}
|
rules = {'armada:test_release': policy_base.RULE_ADMIN_REQUIRED}
|
||||||
self.policy.set_rules(rules)
|
self.policy.set_rules(rules)
|
||||||
resp = self.app.simulate_get('/api/v1.0/test/test-release')
|
resp = self.app.simulate_get(
|
||||||
|
'/api/v1.0/test/test-namespace/test-release')
|
||||||
self.assertEqual(403, resp.status_code)
|
self.assertEqual(403, resp.status_code)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -73,81 +73,8 @@ class TillerControllerTest(base.BaseControllerTest):
|
||||||
mock_tiller.assert_called_once()
|
mock_tiller.assert_called_once()
|
||||||
m_tiller.__exit__.assert_called()
|
m_tiller.__exit__.assert_called()
|
||||||
|
|
||||||
@mock.patch.object(api, 'Tiller')
|
|
||||||
def test_tiller_releases(self, mock_tiller):
|
|
||||||
"""Tests GET /api/v1.0/releases endpoint."""
|
|
||||||
rules = {'tiller:get_release': '@'}
|
|
||||||
self.policy.set_rules(rules)
|
|
||||||
|
|
||||||
def _get_fake_release(name, namespace):
|
|
||||||
fake_release = mock.Mock(namespace='%s_namespace' % namespace)
|
|
||||||
fake_release.configure_mock(name=name)
|
|
||||||
return fake_release
|
|
||||||
|
|
||||||
m_tiller = mock_tiller.return_value
|
|
||||||
m_tiller.__enter__.return_value = m_tiller
|
|
||||||
m_tiller.list_releases.return_value = [
|
|
||||||
_get_fake_release('foo', 'bar'),
|
|
||||||
_get_fake_release('baz', 'qux')
|
|
||||||
]
|
|
||||||
|
|
||||||
result = self.app.simulate_get('/api/v1.0/releases')
|
|
||||||
expected = {
|
|
||||||
'releases': {
|
|
||||||
'bar_namespace': ['foo'],
|
|
||||||
'qux_namespace': ['baz']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.assertEqual(expected, result.json)
|
|
||||||
mock_tiller.assert_called_once()
|
|
||||||
m_tiller.list_releases.assert_called_once_with()
|
|
||||||
m_tiller.__exit__.assert_called()
|
|
||||||
|
|
||||||
@mock.patch.object(api, 'Tiller')
|
|
||||||
def test_tiller_releases_with_params(self, mock_tiller):
|
|
||||||
"""Tests GET /api/v1.0/releases endpoint with query parameters."""
|
|
||||||
rules = {'tiller:get_release': '@'}
|
|
||||||
self.policy.set_rules(rules)
|
|
||||||
|
|
||||||
def _get_fake_release(name, namespace):
|
|
||||||
fake_release = mock.Mock(namespace='%s_namespace' % namespace)
|
|
||||||
fake_release.configure_mock(name=name)
|
|
||||||
return fake_release
|
|
||||||
|
|
||||||
m_tiller = mock_tiller.return_value
|
|
||||||
m_tiller.__enter__.return_value = m_tiller
|
|
||||||
m_tiller.list_releases.return_value = [
|
|
||||||
_get_fake_release('foo', 'bar'),
|
|
||||||
_get_fake_release('baz', 'qux')
|
|
||||||
]
|
|
||||||
|
|
||||||
result = self.app.simulate_get(
|
|
||||||
'/api/v1.0/releases', params_csv=False, params={})
|
|
||||||
expected = {
|
|
||||||
'releases': {
|
|
||||||
'bar_namespace': ['foo'],
|
|
||||||
'qux_namespace': ['baz']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.assertEqual(expected, result.json)
|
|
||||||
mock_tiller.assert_called_once()
|
|
||||||
m_tiller.list_releases.assert_called_once_with()
|
|
||||||
m_tiller.__exit__.assert_called()
|
|
||||||
|
|
||||||
|
|
||||||
class TillerControllerNegativeRbacTest(base.BaseControllerTest):
|
class TillerControllerNegativeRbacTest(base.BaseControllerTest):
|
||||||
@test_utils.attr(type=['negative'])
|
|
||||||
def test_list_tiller_releases_insufficient_permissions(self):
|
|
||||||
"""Tests the GET /api/v1.0/releases endpoint returns 403 following
|
|
||||||
failed authorization.
|
|
||||||
"""
|
|
||||||
rules = {'tiller:get_release': policy_base.RULE_ADMIN_REQUIRED}
|
|
||||||
self.policy.set_rules(rules)
|
|
||||||
resp = self.app.simulate_get('/api/v1.0/releases')
|
|
||||||
self.assertEqual(403, resp.status_code)
|
|
||||||
|
|
||||||
@test_utils.attr(type=['negative'])
|
@test_utils.attr(type=['negative'])
|
||||||
def test_get_tiller_status_insufficient_permissions(self):
|
def test_get_tiller_status_insufficient_permissions(self):
|
||||||
"""Tests the GET /api/v1.0/status endpoint returns 403 following
|
"""Tests the GET /api/v1.0/status endpoint returns 403 following
|
||||||
|
|
|
@ -18,6 +18,6 @@ policy_data = """
|
||||||
"armada:validate_manifest": "rule:admin_required"
|
"armada:validate_manifest": "rule:admin_required"
|
||||||
"armada:test_release": "rule:admin_required"
|
"armada:test_release": "rule:admin_required"
|
||||||
"armada:test_manifest": "rule:admin_required"
|
"armada:test_manifest": "rule:admin_required"
|
||||||
|
"armada:get_release": "rule:admin_required"
|
||||||
"tiller:get_status": "rule:admin_required"
|
"tiller:get_status": "rule:admin_required"
|
||||||
"tiller:get_release": "rule:admin_required"
|
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -12,19 +12,19 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from armada import const
|
from armada import const
|
||||||
from armada.handlers import armada
|
from armada.handlers import armada
|
||||||
from armada.utils.helm import TESTRUN_STATUS_SUCCESS, TESTRUN_STATUS_FAILURE
|
|
||||||
from armada.tests.unit import base
|
from armada.tests.unit import base
|
||||||
from armada.tests.test_utils import AttrDict
|
from armada.handlers import helm
|
||||||
from armada.utils.release import release_prefixer, get_release_status
|
from armada.utils.release import release_prefixer, get_release_status
|
||||||
from armada.exceptions import ManifestException
|
from armada.exceptions import ManifestException
|
||||||
from armada.exceptions.override_exceptions import InvalidOverrideValueException
|
from armada.exceptions.override_exceptions import InvalidOverrideValueException
|
||||||
from armada.exceptions.validate_exceptions import InvalidManifestException
|
from armada.exceptions.validate_exceptions import InvalidManifestException
|
||||||
from armada.exceptions import tiller_exceptions
|
|
||||||
from armada.exceptions.armada_exceptions import ChartDeployException
|
from armada.exceptions.armada_exceptions import ChartDeployException
|
||||||
|
|
||||||
TEST_YAML = """
|
TEST_YAML = """
|
||||||
|
@ -114,11 +114,8 @@ data:
|
||||||
no_hooks: false
|
no_hooks: false
|
||||||
options:
|
options:
|
||||||
force: true
|
force: true
|
||||||
recreate_pods: true
|
|
||||||
test:
|
test:
|
||||||
enabled: true
|
enabled: true
|
||||||
options:
|
|
||||||
cleanup: true
|
|
||||||
---
|
---
|
||||||
schema: armada/Chart/v1
|
schema: armada/Chart/v1
|
||||||
metadata:
|
metadata:
|
||||||
|
@ -150,14 +147,14 @@ CHART_SOURCES = (
|
||||||
|
|
||||||
|
|
||||||
# TODO(seaneagan): Add unit tests with dependencies, including transitive.
|
# TODO(seaneagan): Add unit tests with dependencies, including transitive.
|
||||||
|
def set_source_dir(ch, manifest=None):
|
||||||
|
d = ch['data']
|
||||||
|
d['source_dir'] = (d['source']['location'], d['source']['subpath'])
|
||||||
|
|
||||||
|
|
||||||
class ArmadaHandlerTestCase(base.ArmadaTestCase):
|
class ArmadaHandlerTestCase(base.ArmadaTestCase):
|
||||||
def _test_pre_flight_ops(self, armada_obj, MockChartDownload):
|
def _test_pre_flight_ops(self, armada_obj, MockChartDownload):
|
||||||
def set_source_dir(ch, manifest=None):
|
|
||||||
d = ch['data']
|
|
||||||
d['source_dir'] = (d['source']['location'], d['source']['subpath'])
|
|
||||||
|
|
||||||
MockChartDownload.return_value.get_chart.side_effect = set_source_dir
|
MockChartDownload.return_value.get_chart.side_effect = set_source_dir
|
||||||
|
|
||||||
armada_obj.pre_flight_ops()
|
armada_obj.pre_flight_ops()
|
||||||
|
|
||||||
expected_config = {
|
expected_config = {
|
||||||
|
@ -232,15 +229,11 @@ class ArmadaHandlerTestCase(base.ArmadaTestCase):
|
||||||
'upgrade': {
|
'upgrade': {
|
||||||
'no_hooks': False,
|
'no_hooks': False,
|
||||||
'options': {
|
'options': {
|
||||||
'force': True,
|
'force': True
|
||||||
'recreate_pods': True
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'test': {
|
'test': {
|
||||||
'enabled': True,
|
'enabled': True
|
||||||
'options': {
|
|
||||||
'cleanup': True
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
|
@ -314,9 +307,8 @@ class ArmadaHandlerTestCase(base.ArmadaTestCase):
|
||||||
def test_pre_flight_ops(self, MockChartDownload):
|
def test_pre_flight_ops(self, MockChartDownload):
|
||||||
"""Test pre-flight checks and operations."""
|
"""Test pre-flight checks and operations."""
|
||||||
yaml_documents = list(yaml.safe_load_all(TEST_YAML))
|
yaml_documents = list(yaml.safe_load_all(TEST_YAML))
|
||||||
m_tiller = mock.Mock()
|
m_helm = mock.Mock()
|
||||||
m_tiller.tiller_status.return_value = True
|
armada_obj = armada.Armada(yaml_documents, m_helm)
|
||||||
armada_obj = armada.Armada(yaml_documents, m_tiller)
|
|
||||||
|
|
||||||
self._test_pre_flight_ops(armada_obj, MockChartDownload)
|
self._test_pre_flight_ops(armada_obj, MockChartDownload)
|
||||||
|
|
||||||
|
@ -328,10 +320,9 @@ class ArmadaHandlerTestCase(base.ArmadaTestCase):
|
||||||
yaml_documents = list(yaml.safe_load_all(TEST_YAML))
|
yaml_documents = list(yaml.safe_load_all(TEST_YAML))
|
||||||
|
|
||||||
# Mock methods called by `pre_flight_ops()`.
|
# Mock methods called by `pre_flight_ops()`.
|
||||||
m_tiller = mock.Mock()
|
m_helm = mock.Mock()
|
||||||
m_tiller.tiller_status.return_value = True
|
|
||||||
|
|
||||||
armada_obj = armada.Armada(yaml_documents, m_tiller)
|
armada_obj = armada.Armada(yaml_documents, m_helm)
|
||||||
|
|
||||||
self._test_pre_flight_ops(armada_obj, MockChartDownload)
|
self._test_pre_flight_ops(armada_obj, MockChartDownload)
|
||||||
|
|
||||||
|
@ -359,19 +350,30 @@ class ArmadaHandlerTestCase(base.ArmadaTestCase):
|
||||||
"""Test install functionality from the sync() method."""
|
"""Test install functionality from the sync() method."""
|
||||||
|
|
||||||
@mock.patch.object(armada.Armada, 'post_flight_ops')
|
@mock.patch.object(armada.Armada, 'post_flight_ops')
|
||||||
@mock.patch.object(armada.Armada, 'pre_flight_ops')
|
@mock.patch.object(armada, 'ChartDownload')
|
||||||
@mock.patch('armada.handlers.chart_deploy.ChartBuilder.from_chart_doc')
|
@mock.patch('armada.handlers.chart_deploy.ChartBuilder.from_chart_doc')
|
||||||
@mock.patch('armada.handlers.chart_deploy.Test')
|
@mock.patch('armada.handlers.chart_deploy.Test')
|
||||||
def _do_test(
|
def _do_test(
|
||||||
mock_test, mock_chartbuilder, mock_pre_flight,
|
mock_test, mock_chartbuilder, MockChartDownload,
|
||||||
mock_post_flight):
|
mock_post_flight):
|
||||||
|
MockChartDownload.return_value.get_chart.side_effect = \
|
||||||
|
set_source_dir
|
||||||
# Instantiate Armada object.
|
# Instantiate Armada object.
|
||||||
yaml_documents = list(yaml.safe_load_all(TEST_YAML))
|
yaml_documents = list(yaml.safe_load_all(TEST_YAML))
|
||||||
|
m_helm = mock.MagicMock()
|
||||||
|
armada_obj = armada.Armada(yaml_documents, m_helm)
|
||||||
|
prefix = armada_obj.manifest['data']['release_prefix']
|
||||||
|
|
||||||
m_tiller = mock.MagicMock()
|
def release_metadata(release_id, **kwargs):
|
||||||
m_tiller.list_releases.return_value = known_releases
|
try:
|
||||||
|
return next(
|
||||||
|
r for r in known_releases
|
||||||
|
if release_id.name == r['name']
|
||||||
|
and release_id.namespace == r['namespace'])
|
||||||
|
except StopIteration:
|
||||||
|
return None
|
||||||
|
|
||||||
armada_obj = armada.Armada(yaml_documents, m_tiller)
|
m_helm.release_metadata.side_effect = release_metadata
|
||||||
armada_obj.chart_deploy.get_diff = mock.Mock()
|
armada_obj.chart_deploy.get_diff = mock.Mock()
|
||||||
|
|
||||||
cg = armada_obj.manifest['data']['chart_groups'][0]
|
cg = armada_obj.manifest['data']['chart_groups'][0]
|
||||||
|
@ -379,18 +381,12 @@ class ArmadaHandlerTestCase(base.ArmadaTestCase):
|
||||||
charts = chart_group['chart_group']
|
charts = chart_group['chart_group']
|
||||||
cg_test_all_charts = chart_group.get('test_charts')
|
cg_test_all_charts = chart_group.get('test_charts')
|
||||||
|
|
||||||
mock_test_release = mock_test.return_value.test_release_for_success
|
mock_test_release = mock_test.return_value.test_release
|
||||||
if test_failure_to_run:
|
if test_failure_to_run:
|
||||||
|
mock_test_release.side_effect = Exception('test failed to run')
|
||||||
def fail(tiller, release, timeout=None, cleanup=False):
|
|
||||||
status = AttrDict(
|
|
||||||
**{'info': AttrDict(**{'Description': 'Failed'})})
|
|
||||||
raise tiller_exceptions.ReleaseException(
|
|
||||||
release, status, 'Test')
|
|
||||||
|
|
||||||
mock_test_release.side_effect = fail
|
|
||||||
else:
|
else:
|
||||||
mock_test_release.return_value = test_success
|
if not test_success:
|
||||||
|
mock_test_release.side_effect = Exception('test failed')
|
||||||
mock_test.return_value.timeout = const.DEFAULT_TEST_TIMEOUT
|
mock_test.return_value.timeout = const.DEFAULT_TEST_TIMEOUT
|
||||||
|
|
||||||
# Stub out irrelevant methods called by `armada.sync()`.
|
# Stub out irrelevant methods called by `armada.sync()`.
|
||||||
|
@ -402,56 +398,53 @@ class ArmadaHandlerTestCase(base.ArmadaTestCase):
|
||||||
armada_obj.sync()
|
armada_obj.sync()
|
||||||
|
|
||||||
expected_install_release_calls = []
|
expected_install_release_calls = []
|
||||||
expected_update_release_calls = []
|
expected_upgrade_release_calls = []
|
||||||
expected_uninstall_release_calls = []
|
expected_uninstall_release_calls = []
|
||||||
expected_test_constructor_calls = []
|
expected_test_constructor_calls = []
|
||||||
|
|
||||||
for c in charts:
|
for c in charts:
|
||||||
chart = c['data']
|
chart = c['data']
|
||||||
release = chart['release']
|
release = chart['release']
|
||||||
prefix = armada_obj.manifest['data']['release_prefix']
|
|
||||||
release_name = release_prefixer(prefix, release)
|
release_name = release_prefixer(prefix, release)
|
||||||
|
release_id = helm.HelmReleaseId(
|
||||||
|
chart['namespace'], release_name)
|
||||||
|
source_dir = chart['source_dir']
|
||||||
|
source_directory = os.path.join(*source_dir)
|
||||||
|
|
||||||
# Simplified check because the actual code uses logical-or's
|
# Simplified check because the actual code uses logical-or's
|
||||||
# multiple conditions, so this is enough.
|
# multiple conditions, so this is enough.
|
||||||
native_wait_enabled = (
|
native_wait_enabled = (
|
||||||
chart['wait'].get('native', {}).get('enabled', True))
|
chart['wait'].get('native', {}).get('enabled', True))
|
||||||
|
|
||||||
if release_name not in [x.name for x in known_releases]:
|
if release_name not in [x['name'] for x in known_releases]:
|
||||||
expected_install_release_calls.append(
|
expected_install_release_calls.append(
|
||||||
mock.call(
|
mock.call(
|
||||||
mock_chartbuilder().get_helm_chart(),
|
source_directory,
|
||||||
"{}-{}".format(
|
release_id,
|
||||||
armada_obj.manifest['data']['release_prefix'],
|
values=chart['values'],
|
||||||
chart['release']),
|
|
||||||
chart['namespace'],
|
|
||||||
values=yaml.safe_dump(chart['values']),
|
|
||||||
wait=native_wait_enabled,
|
wait=native_wait_enabled,
|
||||||
timeout=mock.ANY))
|
timeout=mock.ANY))
|
||||||
else:
|
else:
|
||||||
target_release = None
|
target_release = None
|
||||||
for known_release in known_releases:
|
for known_release in known_releases:
|
||||||
if known_release.name == release_name:
|
if known_release['name'] == release_name:
|
||||||
target_release = known_release
|
target_release = known_release
|
||||||
break
|
break
|
||||||
if target_release:
|
if target_release:
|
||||||
status = get_release_status(target_release)
|
status = get_release_status(target_release)
|
||||||
if status == const.STATUS_FAILED:
|
if status == helm.STATUS_FAILED:
|
||||||
protected = chart.get('protected', {})
|
protected = chart.get('protected', {})
|
||||||
if not protected:
|
if not protected:
|
||||||
expected_uninstall_release_calls.append(
|
expected_uninstall_release_calls.append(
|
||||||
mock.call(
|
mock.call(
|
||||||
release_name,
|
release_id,
|
||||||
purge=True,
|
purge=True,
|
||||||
timeout=const.DEFAULT_DELETE_TIMEOUT))
|
timeout=const.DEFAULT_DELETE_TIMEOUT))
|
||||||
expected_install_release_calls.append(
|
expected_install_release_calls.append(
|
||||||
mock.call(
|
mock.call(
|
||||||
mock_chartbuilder().get_helm_chart(),
|
source_directory,
|
||||||
"{}-{}".format(
|
release_id,
|
||||||
armada_obj.manifest['data']
|
values=chart['values'],
|
||||||
['release_prefix'],
|
|
||||||
chart['release']),
|
|
||||||
chart['namespace'],
|
|
||||||
values=yaml.safe_dump(chart['values']),
|
|
||||||
wait=native_wait_enabled,
|
wait=native_wait_enabled,
|
||||||
timeout=mock.ANY))
|
timeout=mock.ANY))
|
||||||
else:
|
else:
|
||||||
|
@ -463,62 +456,55 @@ class ArmadaHandlerTestCase(base.ArmadaTestCase):
|
||||||
if chart_group['sequenced']:
|
if chart_group['sequenced']:
|
||||||
break
|
break
|
||||||
|
|
||||||
if status == const.STATUS_DEPLOYED:
|
if status == helm.STATUS_DEPLOYED:
|
||||||
if diff:
|
if diff:
|
||||||
upgrade = chart.get('upgrade', {})
|
upgrade = chart.get('upgrade', {})
|
||||||
disable_hooks = upgrade.get('no_hooks', False)
|
disable_hooks = upgrade.get('no_hooks', False)
|
||||||
options = upgrade.get('options', {})
|
options = upgrade.get('options', {})
|
||||||
force = options.get('force', False)
|
force = options.get('force', False)
|
||||||
recreate_pods = options.get(
|
|
||||||
'recreate_pods', False)
|
|
||||||
|
|
||||||
expected_update_release_calls.append(
|
expected_upgrade_release_calls.append(
|
||||||
mock.call(
|
mock.call(
|
||||||
mock_chartbuilder().get_helm_chart(),
|
source_directory,
|
||||||
"{}-{}".format(
|
release_id,
|
||||||
armada_obj.manifest['data']
|
|
||||||
['release_prefix'],
|
|
||||||
chart['release']),
|
|
||||||
chart['namespace'],
|
|
||||||
disable_hooks=disable_hooks,
|
disable_hooks=disable_hooks,
|
||||||
force=force,
|
force=force,
|
||||||
recreate_pods=recreate_pods,
|
values=chart['values'],
|
||||||
values=yaml.safe_dump(chart['values']),
|
|
||||||
wait=native_wait_enabled,
|
wait=native_wait_enabled,
|
||||||
timeout=mock.ANY))
|
timeout=mock.ANY))
|
||||||
|
|
||||||
expected_test_constructor_calls.append(
|
expected_test_constructor_calls.append(
|
||||||
mock.call(
|
mock.call(
|
||||||
chart,
|
chart,
|
||||||
release_name,
|
release_id,
|
||||||
m_tiller,
|
m_helm,
|
||||||
cg_test_charts=cg_test_all_charts))
|
cg_test_charts=cg_test_all_charts))
|
||||||
|
|
||||||
any_order = not chart_group['sequenced']
|
any_order = not chart_group['sequenced']
|
||||||
# Verify that at least 1 release is either installed or updated.
|
# Verify that at least 1 release is either installed or updated.
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
len(expected_install_release_calls) >= 1
|
len(expected_install_release_calls) >= 1
|
||||||
or len(expected_update_release_calls) >= 1)
|
or len(expected_upgrade_release_calls) >= 1)
|
||||||
# Verify that the expected number of non-deployed releases are
|
# Verify that the expected number of non-deployed releases are
|
||||||
# installed with expected arguments.
|
# installed with expected arguments.
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
len(expected_install_release_calls),
|
len(expected_install_release_calls),
|
||||||
m_tiller.install_release.call_count)
|
m_helm.install_release.call_count)
|
||||||
m_tiller.install_release.assert_has_calls(
|
m_helm.install_release.assert_has_calls(
|
||||||
expected_install_release_calls, any_order=any_order)
|
expected_install_release_calls, any_order=any_order)
|
||||||
# Verify that the expected number of deployed releases are
|
# Verify that the expected number of deployed releases are
|
||||||
# updated with expected arguments.
|
# updated with expected arguments.
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
len(expected_update_release_calls),
|
len(expected_upgrade_release_calls),
|
||||||
m_tiller.update_release.call_count)
|
m_helm.upgrade_release.call_count)
|
||||||
m_tiller.update_release.assert_has_calls(
|
m_helm.upgrade_release.assert_has_calls(
|
||||||
expected_update_release_calls, any_order=any_order)
|
expected_upgrade_release_calls, any_order=any_order)
|
||||||
# Verify that the expected number of deployed releases are
|
# Verify that the expected number of deployed releases are
|
||||||
# uninstalled with expected arguments.
|
# uninstalled with expected arguments.
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
len(expected_uninstall_release_calls),
|
len(expected_uninstall_release_calls),
|
||||||
m_tiller.uninstall_release.call_count)
|
m_helm.uninstall_release.call_count)
|
||||||
m_tiller.uninstall_release.assert_has_calls(
|
m_helm.uninstall_release.assert_has_calls(
|
||||||
expected_uninstall_release_calls, any_order=any_order)
|
expected_uninstall_release_calls, any_order=any_order)
|
||||||
# Verify that the expected number of deployed releases are
|
# Verify that the expected number of deployed releases are
|
||||||
# tested with expected arguments.
|
# tested with expected arguments.
|
||||||
|
@ -537,37 +523,31 @@ class ArmadaHandlerTestCase(base.ArmadaTestCase):
|
||||||
c for c in yaml_documents if c['data'].get('chart_name') == name
|
c for c in yaml_documents if c['data'].get('chart_name') == name
|
||||||
][0]
|
][0]
|
||||||
|
|
||||||
def get_mock_release(self, name, status, last_test_results=None):
|
def get_mock_release(self, name, status, last_test_results=[]):
|
||||||
status_mock = mock.Mock()
|
|
||||||
status_mock.return_value = status
|
|
||||||
chart = self._get_chart_by_name(name)
|
chart = self._get_chart_by_name(name)
|
||||||
|
|
||||||
def get_test_result(success):
|
def get_test_hook(index, success):
|
||||||
status = (
|
return {
|
||||||
TESTRUN_STATUS_SUCCESS if success else TESTRUN_STATUS_FAILURE)
|
"kind": "Pod",
|
||||||
return mock.Mock(status=status)
|
"events": ["test"],
|
||||||
|
"last_run": {
|
||||||
|
"phase": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
last_test_suite_run = None
|
hooks = [get_test_hook(i, r) for i, r in enumerate(last_test_results)]
|
||||||
if last_test_results is not None:
|
|
||||||
results = [get_test_result(r) for r in last_test_results]
|
|
||||||
last_test_suite_run = mock.Mock(results=results)
|
|
||||||
|
|
||||||
def has_last_test(name):
|
return {
|
||||||
if name == 'last_test_suite_run':
|
"name": name,
|
||||||
return last_test_results is not None
|
"namespace": "test",
|
||||||
self.fail('Called HasField() with unexpected field')
|
"version": 1,
|
||||||
|
"chart": chart,
|
||||||
mock_release = mock.Mock(
|
"config": {},
|
||||||
version=1,
|
"info": {
|
||||||
chart=chart,
|
"status": status
|
||||||
config=mock.Mock(raw="{}"),
|
},
|
||||||
info=mock.Mock(
|
"hooks": hooks
|
||||||
status=mock.Mock(
|
}
|
||||||
Code=mock.MagicMock(Name=status_mock),
|
|
||||||
HasField=mock.MagicMock(side_effect=has_last_test),
|
|
||||||
last_test_suite_run=last_test_suite_run)))
|
|
||||||
mock_release.name = name
|
|
||||||
return mock_release
|
|
||||||
|
|
||||||
def test_armada_sync_with_no_deployed_releases(self):
|
def test_armada_sync_with_no_deployed_releases(self):
|
||||||
known_releases = []
|
known_releases = []
|
||||||
|
@ -576,13 +556,13 @@ class ArmadaHandlerTestCase(base.ArmadaTestCase):
|
||||||
def test_armada_sync_with_one_deployed_release(self):
|
def test_armada_sync_with_one_deployed_release(self):
|
||||||
c1 = 'armada-test_chart_1'
|
c1 = 'armada-test_chart_1'
|
||||||
|
|
||||||
known_releases = [self.get_mock_release(c1, const.STATUS_DEPLOYED)]
|
known_releases = [self.get_mock_release(c1, helm.STATUS_DEPLOYED)]
|
||||||
self._test_sync(known_releases)
|
self._test_sync(known_releases)
|
||||||
|
|
||||||
def test_armada_sync_with_one_deployed_release_no_diff(self):
|
def test_armada_sync_with_one_deployed_release_no_diff(self):
|
||||||
c1 = 'armada-test_chart_1'
|
c1 = 'armada-test_chart_1'
|
||||||
|
|
||||||
known_releases = [self.get_mock_release(c1, const.STATUS_DEPLOYED)]
|
known_releases = [self.get_mock_release(c1, helm.STATUS_DEPLOYED)]
|
||||||
self._test_sync(known_releases, diff=set())
|
self._test_sync(known_releases, diff=set())
|
||||||
|
|
||||||
def test_armada_sync_with_failed_test_result(self):
|
def test_armada_sync_with_failed_test_result(self):
|
||||||
|
@ -590,7 +570,7 @@ class ArmadaHandlerTestCase(base.ArmadaTestCase):
|
||||||
|
|
||||||
known_releases = [
|
known_releases = [
|
||||||
self.get_mock_release(
|
self.get_mock_release(
|
||||||
c1, const.STATUS_DEPLOYED, last_test_results=[False])
|
c1, helm.STATUS_DEPLOYED, last_test_results=[False])
|
||||||
]
|
]
|
||||||
self._test_sync(
|
self._test_sync(
|
||||||
known_releases, diff=set(), expected_last_test_result=False)
|
known_releases, diff=set(), expected_last_test_result=False)
|
||||||
|
@ -600,7 +580,7 @@ class ArmadaHandlerTestCase(base.ArmadaTestCase):
|
||||||
|
|
||||||
known_releases = [
|
known_releases = [
|
||||||
self.get_mock_release(
|
self.get_mock_release(
|
||||||
c1, const.STATUS_DEPLOYED, last_test_results=[True])
|
c1, helm.STATUS_DEPLOYED, last_test_results=[True])
|
||||||
]
|
]
|
||||||
self._test_sync(
|
self._test_sync(
|
||||||
known_releases, diff=set(), expected_last_test_result=True)
|
known_releases, diff=set(), expected_last_test_result=True)
|
||||||
|
@ -610,7 +590,7 @@ class ArmadaHandlerTestCase(base.ArmadaTestCase):
|
||||||
|
|
||||||
known_releases = [
|
known_releases = [
|
||||||
self.get_mock_release(
|
self.get_mock_release(
|
||||||
c1, const.STATUS_DEPLOYED, last_test_results=[])
|
c1, helm.STATUS_DEPLOYED, last_test_results=[])
|
||||||
]
|
]
|
||||||
self._test_sync(
|
self._test_sync(
|
||||||
known_releases, diff=set(), expected_last_test_result=True)
|
known_releases, diff=set(), expected_last_test_result=True)
|
||||||
|
@ -620,15 +600,15 @@ class ArmadaHandlerTestCase(base.ArmadaTestCase):
|
||||||
c2 = 'armada-test_chart_2'
|
c2 = 'armada-test_chart_2'
|
||||||
|
|
||||||
known_releases = [
|
known_releases = [
|
||||||
self.get_mock_release(c1, const.STATUS_DEPLOYED),
|
self.get_mock_release(c1, helm.STATUS_DEPLOYED),
|
||||||
self.get_mock_release(c2, const.STATUS_DEPLOYED)
|
self.get_mock_release(c2, helm.STATUS_DEPLOYED)
|
||||||
]
|
]
|
||||||
self._test_sync(known_releases)
|
self._test_sync(known_releases)
|
||||||
|
|
||||||
def test_armada_sync_with_unprotected_releases(self):
|
def test_armada_sync_with_unprotected_releases(self):
|
||||||
c1 = 'armada-test_chart_1'
|
c1 = 'armada-test_chart_1'
|
||||||
|
|
||||||
known_releases = [self.get_mock_release(c1, const.STATUS_FAILED)]
|
known_releases = [self.get_mock_release(c1, helm.STATUS_FAILED)]
|
||||||
self._test_sync(known_releases)
|
self._test_sync(known_releases)
|
||||||
|
|
||||||
def test_armada_sync_with_protected_releases_continue(self):
|
def test_armada_sync_with_protected_releases_continue(self):
|
||||||
|
@ -636,15 +616,15 @@ class ArmadaHandlerTestCase(base.ArmadaTestCase):
|
||||||
c2 = 'armada-test_chart_2'
|
c2 = 'armada-test_chart_2'
|
||||||
|
|
||||||
known_releases = [
|
known_releases = [
|
||||||
self.get_mock_release(c2, const.STATUS_FAILED),
|
self.get_mock_release(c2, helm.STATUS_FAILED),
|
||||||
self.get_mock_release(c1, const.STATUS_FAILED)
|
self.get_mock_release(c1, helm.STATUS_FAILED)
|
||||||
]
|
]
|
||||||
self._test_sync(known_releases)
|
self._test_sync(known_releases)
|
||||||
|
|
||||||
def test_armada_sync_with_protected_releases_halt(self):
|
def test_armada_sync_with_protected_releases_halt(self):
|
||||||
c3 = 'armada-test_chart_3'
|
c3 = 'armada-test_chart_3'
|
||||||
|
|
||||||
known_releases = [self.get_mock_release(c3, const.STATUS_FAILED)]
|
known_releases = [self.get_mock_release(c3, helm.STATUS_FAILED)]
|
||||||
|
|
||||||
def _test_method():
|
def _test_method():
|
||||||
self._test_sync(known_releases)
|
self._test_sync(known_releases)
|
||||||
|
|
|
@ -12,13 +12,11 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import inspect
|
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
import fixtures
|
import fixtures
|
||||||
from hapi.chart.chart_pb2 import Chart
|
|
||||||
from hapi.chart.metadata_pb2 import Metadata
|
|
||||||
import mock
|
import mock
|
||||||
import testtools
|
import testtools
|
||||||
import yaml
|
import yaml
|
||||||
|
@ -36,30 +34,7 @@ class BaseChartBuilderTestCase(testtools.TestCase):
|
||||||
version: 0.1.0
|
version: 0.1.0
|
||||||
"""
|
"""
|
||||||
|
|
||||||
chart_value = """
|
chart_doc_yaml = """
|
||||||
# Default values for hello-world-chart.
|
|
||||||
# This is a YAML-formatted file.
|
|
||||||
# Declare variables to be passed into your templates.
|
|
||||||
replicaCount: 1
|
|
||||||
image:
|
|
||||||
repository: nginx
|
|
||||||
tag: stable
|
|
||||||
pullPolicy: IfNotPresent
|
|
||||||
service:
|
|
||||||
name: nginx
|
|
||||||
type: ClusterIP
|
|
||||||
externalPort: 38443
|
|
||||||
internalPort: 80
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpu: 100m
|
|
||||||
memory: 128Mi
|
|
||||||
requests:
|
|
||||||
cpu: 100m
|
|
||||||
memory: 128Mi
|
|
||||||
"""
|
|
||||||
|
|
||||||
chart_stream = """
|
|
||||||
schema: armada/Chart/v1
|
schema: armada/Chart/v1
|
||||||
metadata:
|
metadata:
|
||||||
name: test
|
name: test
|
||||||
|
@ -83,14 +58,14 @@ class BaseChartBuilderTestCase(testtools.TestCase):
|
||||||
dependencies: []
|
dependencies: []
|
||||||
"""
|
"""
|
||||||
|
|
||||||
dependency_chart_yaml = """
|
dep_chart_yaml = """
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
description: Another sample Helm chart for Kubernetes
|
description: Another sample Helm chart for Kubernetes
|
||||||
name: dependency-chart
|
name: dependency-chart
|
||||||
version: 0.1.0
|
version: 0.1.0
|
||||||
"""
|
"""
|
||||||
|
|
||||||
dependency_chart_stream = """
|
dep_chart_doc_yaml = """
|
||||||
schema: armada/Chart/v1
|
schema: armada/Chart/v1
|
||||||
metadata:
|
metadata:
|
||||||
name: dep
|
name: dep
|
||||||
|
@ -120,13 +95,6 @@ class BaseChartBuilderTestCase(testtools.TestCase):
|
||||||
finally:
|
finally:
|
||||||
os.close(fd)
|
os.close(fd)
|
||||||
|
|
||||||
def _make_temporary_subdirectory(self, parent_path, sub_path):
|
|
||||||
subdir = os.path.join(parent_path, sub_path)
|
|
||||||
if not os.path.exists(subdir):
|
|
||||||
os.makedirs(subdir)
|
|
||||||
self.addCleanup(shutil.rmtree, subdir)
|
|
||||||
return subdir
|
|
||||||
|
|
||||||
def _get_test_chart(self, chart_dir):
|
def _get_test_chart(self, chart_dir):
|
||||||
return {
|
return {
|
||||||
'schema': 'armada/Chart/v1',
|
'schema': 'armada/Chart/v1',
|
||||||
|
@ -134,379 +102,67 @@ class BaseChartBuilderTestCase(testtools.TestCase):
|
||||||
'name': 'test'
|
'name': 'test'
|
||||||
},
|
},
|
||||||
const.KEYWORD_DATA: {
|
const.KEYWORD_DATA: {
|
||||||
'source_dir': (chart_dir.path, '')
|
'source_dir': (chart_dir, '')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ChartBuilderTestCase(BaseChartBuilderTestCase):
|
class ChartBuilderTestCase(BaseChartBuilderTestCase):
|
||||||
def test_source_clone(self):
|
def test_get_helm_chart_success(self):
|
||||||
# Create a temporary directory with Chart.yaml that contains data
|
|
||||||
# from ``self.chart_yaml``.
|
|
||||||
chart_dir = self.useFixture(fixtures.TempDir())
|
chart_dir = self.useFixture(fixtures.TempDir())
|
||||||
self.addCleanup(shutil.rmtree, chart_dir.path)
|
self.addCleanup(shutil.rmtree, chart_dir.path)
|
||||||
self._write_temporary_file_contents(
|
helm_mock = mock.Mock()
|
||||||
chart_dir.path, 'Chart.yaml', self.chart_yaml)
|
helm_mock.upgrade_release.return_value = {"chart": mock.sentinel.chart}
|
||||||
|
|
||||||
chartbuilder = ChartBuilder.from_chart_doc(
|
chartbuilder = ChartBuilder.from_chart_doc(
|
||||||
self._get_test_chart(chart_dir))
|
self._get_test_chart(chart_dir.path), helm_mock)
|
||||||
|
release_id = mock.Mock()
|
||||||
|
values = mock.Mock()
|
||||||
|
actual_chart = chartbuilder.get_helm_chart(release_id, values)
|
||||||
|
self.assertIs(mock.sentinel.chart, actual_chart)
|
||||||
|
|
||||||
# Validate response type is :class:`hapi.chart.metadata_pb2.Metadata`
|
def test_get_helm_chart_fail(self):
|
||||||
resp = chartbuilder.get_metadata()
|
|
||||||
self.assertIsInstance(resp, Metadata)
|
|
||||||
|
|
||||||
def test_get_metadata_with_incorrect_file_invalid(self):
|
|
||||||
chart_dir = self.useFixture(fixtures.TempDir())
|
chart_dir = self.useFixture(fixtures.TempDir())
|
||||||
self.addCleanup(shutil.rmtree, chart_dir.path)
|
self.addCleanup(shutil.rmtree, chart_dir.path)
|
||||||
|
helm_mock = mock.Mock()
|
||||||
|
helm_mock.upgrade_release.side_effect = Exception()
|
||||||
chartbuilder = ChartBuilder.from_chart_doc(
|
chartbuilder = ChartBuilder.from_chart_doc(
|
||||||
self._get_test_chart(chart_dir))
|
self._get_test_chart(chart_dir.path), helm_mock)
|
||||||
|
|
||||||
|
def test():
|
||||||
|
release_id = mock.Mock()
|
||||||
|
values = mock.Mock()
|
||||||
|
chartbuilder.get_helm_chart(release_id, values)
|
||||||
|
|
||||||
self.assertRaises(
|
self.assertRaises(
|
||||||
chartbuilder_exceptions.MetadataLoadException,
|
chartbuilder_exceptions.HelmChartBuildException, test)
|
||||||
chartbuilder.get_metadata)
|
|
||||||
|
|
||||||
def test_get_files(self):
|
def test_dependency_resolution(self):
|
||||||
"""Validates that ``get_files()`` ignores 'Chart.yaml', 'values.yaml'
|
|
||||||
and 'templates' subfolder and all the files contained therein.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Create a temporary directory that represents a chart source directory
|
|
||||||
# with various files, including 'Chart.yaml' and 'values.yaml' which
|
|
||||||
# should be ignored by `get_files()`.
|
|
||||||
chart_dir = self.useFixture(fixtures.TempDir())
|
|
||||||
self.addCleanup(shutil.rmtree, chart_dir.path)
|
|
||||||
for filename in ['foo', 'bar', 'Chart.yaml', 'values.yaml']:
|
|
||||||
self._write_temporary_file_contents(chart_dir.path, filename, "")
|
|
||||||
|
|
||||||
# Create a template directory -- 'templates' -- nested inside the chart
|
|
||||||
# directory which should also be ignored.
|
|
||||||
templates_subdir = self._make_temporary_subdirectory(
|
|
||||||
chart_dir.path, 'templates')
|
|
||||||
for filename in ['template%d' % x for x in range(3)]:
|
|
||||||
self._write_temporary_file_contents(templates_subdir, filename, "")
|
|
||||||
|
|
||||||
chartbuilder = ChartBuilder.from_chart_doc(
|
|
||||||
self._get_test_chart(chart_dir))
|
|
||||||
|
|
||||||
expected_files = (
|
|
||||||
'[type_url: "%s"\n, type_url: "%s"\n]' % ('./bar', './foo'))
|
|
||||||
# Validate that only 'foo' and 'bar' are returned.
|
|
||||||
actual_files = sorted(
|
|
||||||
chartbuilder.get_files(), key=lambda x: x.type_url)
|
|
||||||
self.assertEqual(expected_files, repr(actual_files).strip())
|
|
||||||
|
|
||||||
def test_get_files_with_unicode_characters(self):
|
|
||||||
chart_dir = self.useFixture(fixtures.TempDir())
|
|
||||||
self.addCleanup(shutil.rmtree, chart_dir.path)
|
|
||||||
for filename in ['foo', 'bar', 'Chart.yaml', 'values.yaml']:
|
|
||||||
self._write_temporary_file_contents(
|
|
||||||
chart_dir.path, filename, "DIRC^@^@^@^B^@^@^@×Z®<86>F.1")
|
|
||||||
|
|
||||||
chartbuilder = ChartBuilder.from_chart_doc(
|
|
||||||
self._get_test_chart(chart_dir))
|
|
||||||
chartbuilder.get_files()
|
|
||||||
|
|
||||||
def test_get_basic_helm_chart(self):
|
|
||||||
# Before ChartBuilder is executed the `source_dir` points to a
|
|
||||||
# directory that was either clone or unpacked from a tarball... pretend
|
|
||||||
# that that logic has already been performed.
|
|
||||||
chart_dir = self.useFixture(fixtures.TempDir())
|
|
||||||
self.addCleanup(shutil.rmtree, chart_dir.path)
|
|
||||||
self._write_temporary_file_contents(
|
|
||||||
chart_dir.path, 'Chart.yaml', self.chart_yaml)
|
|
||||||
ch = yaml.safe_load(self.chart_stream)
|
|
||||||
ch['data']['source_dir'] = (chart_dir.path, '')
|
|
||||||
|
|
||||||
test_chart = ch
|
|
||||||
chartbuilder = ChartBuilder.from_chart_doc(test_chart)
|
|
||||||
helm_chart = chartbuilder.get_helm_chart()
|
|
||||||
|
|
||||||
expected = inspect.cleandoc(
|
|
||||||
"""
|
|
||||||
metadata {
|
|
||||||
name: "hello-world-chart"
|
|
||||||
version: "0.1.0"
|
|
||||||
description: "A sample Helm chart for Kubernetes"
|
|
||||||
}
|
|
||||||
values {
|
|
||||||
}
|
|
||||||
""").strip()
|
|
||||||
|
|
||||||
self.assertIsInstance(helm_chart, Chart)
|
|
||||||
self.assertTrue(hasattr(helm_chart, 'metadata'))
|
|
||||||
self.assertTrue(hasattr(helm_chart, 'values'))
|
|
||||||
self.assertEqual(expected, repr(helm_chart).strip())
|
|
||||||
|
|
||||||
def test_get_helm_chart_with_values(self):
|
|
||||||
chart_dir = self.useFixture(fixtures.TempDir())
|
|
||||||
self.addCleanup(shutil.rmtree, chart_dir.path)
|
|
||||||
|
|
||||||
self._write_temporary_file_contents(
|
|
||||||
chart_dir.path, 'Chart.yaml', self.chart_yaml)
|
|
||||||
self._write_temporary_file_contents(
|
|
||||||
chart_dir.path, 'values.yaml', self.chart_value)
|
|
||||||
|
|
||||||
ch = yaml.safe_load(self.chart_stream)
|
|
||||||
ch['data']['source_dir'] = (chart_dir.path, '')
|
|
||||||
|
|
||||||
test_chart = ch
|
|
||||||
chartbuilder = ChartBuilder.from_chart_doc(test_chart)
|
|
||||||
helm_chart = chartbuilder.get_helm_chart()
|
|
||||||
|
|
||||||
self.assertIsInstance(helm_chart, Chart)
|
|
||||||
self.assertTrue(hasattr(helm_chart, 'metadata'))
|
|
||||||
self.assertTrue(hasattr(helm_chart, 'values'))
|
|
||||||
self.assertTrue(hasattr(helm_chart.values, 'raw'))
|
|
||||||
self.assertEqual(self.chart_value, helm_chart.values.raw)
|
|
||||||
|
|
||||||
def test_get_helm_chart_with_files(self):
|
|
||||||
# Create a chart directory with some test files.
|
|
||||||
chart_dir = self.useFixture(fixtures.TempDir())
|
|
||||||
self.addCleanup(shutil.rmtree, chart_dir.path)
|
|
||||||
# Chart.yaml is mandatory for `ChartBuilder.get_metadata`.
|
|
||||||
self._write_temporary_file_contents(
|
|
||||||
chart_dir.path, 'Chart.yaml', self.chart_yaml)
|
|
||||||
self._write_temporary_file_contents(chart_dir.path, 'foo', "foobar")
|
|
||||||
self._write_temporary_file_contents(chart_dir.path, 'bar', "bazqux")
|
|
||||||
|
|
||||||
# Also create a nested directory and verify that files from it are also
|
|
||||||
# added.
|
|
||||||
nested_dir = self._make_temporary_subdirectory(
|
|
||||||
chart_dir.path, 'nested')
|
|
||||||
self._write_temporary_file_contents(nested_dir, 'nested0', "random")
|
|
||||||
|
|
||||||
ch = yaml.safe_load(self.chart_stream)
|
|
||||||
ch['data']['source_dir'] = (chart_dir.path, '')
|
|
||||||
|
|
||||||
test_chart = ch
|
|
||||||
chartbuilder = ChartBuilder.from_chart_doc(test_chart)
|
|
||||||
helm_chart = chartbuilder.get_helm_chart()
|
|
||||||
|
|
||||||
expected_files = (
|
|
||||||
'[type_url: "%s"\nvalue: "bazqux"\n, '
|
|
||||||
'type_url: "%s"\nvalue: "foobar"\n, '
|
|
||||||
'type_url: "%s"\nvalue: "random"\n]' %
|
|
||||||
('./bar', './foo', 'nested/nested0'))
|
|
||||||
|
|
||||||
self.assertIsInstance(helm_chart, Chart)
|
|
||||||
self.assertTrue(hasattr(helm_chart, 'metadata'))
|
|
||||||
self.assertTrue(hasattr(helm_chart, 'values'))
|
|
||||||
self.assertTrue(hasattr(helm_chart, 'files'))
|
|
||||||
actual_files = sorted(helm_chart.files, key=lambda x: x.value)
|
|
||||||
self.assertEqual(expected_files, repr(actual_files).strip())
|
|
||||||
|
|
||||||
def test_get_helm_chart_includes_only_relevant_files(self):
|
|
||||||
chart_dir = self.useFixture(fixtures.TempDir())
|
|
||||||
self.addCleanup(shutil.rmtree, chart_dir.path)
|
|
||||||
|
|
||||||
templates_subdir = self._make_temporary_subdirectory(
|
|
||||||
chart_dir.path, 'templates')
|
|
||||||
charts_subdir = self._make_temporary_subdirectory(
|
|
||||||
chart_dir.path, 'charts')
|
|
||||||
templates_nested_subdir = self._make_temporary_subdirectory(
|
|
||||||
templates_subdir, 'bin')
|
|
||||||
charts_nested_subdir = self._make_temporary_subdirectory(
|
|
||||||
charts_subdir, 'extra')
|
|
||||||
|
|
||||||
self._write_temporary_file_contents(
|
|
||||||
chart_dir.path, 'Chart.yaml', self.chart_yaml)
|
|
||||||
self._write_temporary_file_contents(chart_dir.path, 'foo', "foobar")
|
|
||||||
self._write_temporary_file_contents(chart_dir.path, 'bar', "bazqux")
|
|
||||||
|
|
||||||
# Files to ignore within top-level directory.
|
|
||||||
files_to_ignore = ['Chart.yaml', 'values.yaml', 'values.toml']
|
|
||||||
for file in files_to_ignore:
|
|
||||||
self._write_temporary_file_contents(chart_dir.path, file, "")
|
|
||||||
file_to_ignore = 'file_to_ignore'
|
|
||||||
# Files to ignore within templates/ subdirectory.
|
|
||||||
self._write_temporary_file_contents(
|
|
||||||
templates_subdir, file_to_ignore, "")
|
|
||||||
# Files to ignore within templates/bin subdirectory.
|
|
||||||
self._write_temporary_file_contents(
|
|
||||||
templates_nested_subdir, file_to_ignore, "")
|
|
||||||
# Files to ignore within charts/extra subdirectory.
|
|
||||||
self._write_temporary_file_contents(
|
|
||||||
charts_nested_subdir, file_to_ignore, "")
|
|
||||||
self._write_temporary_file_contents(
|
|
||||||
charts_nested_subdir, 'Chart.yaml', self.chart_yaml)
|
|
||||||
# Files to **include** within charts/ subdirectory.
|
|
||||||
self._write_temporary_file_contents(charts_subdir, '.prov', "xyzzy")
|
|
||||||
|
|
||||||
ch = yaml.safe_load(self.chart_stream)
|
|
||||||
ch['data']['source_dir'] = (chart_dir.path, '')
|
|
||||||
|
|
||||||
test_chart = ch
|
|
||||||
chartbuilder = ChartBuilder.from_chart_doc(test_chart)
|
|
||||||
helm_chart = chartbuilder.get_helm_chart()
|
|
||||||
|
|
||||||
expected_files = (
|
|
||||||
'[type_url: "%s"\nvalue: "bazqux"\n, '
|
|
||||||
'type_url: "%s"\nvalue: "foobar"\n, '
|
|
||||||
'type_url: "%s"\nvalue: "xyzzy"\n]' %
|
|
||||||
('./bar', './foo', 'charts/.prov'))
|
|
||||||
|
|
||||||
# Validate that only relevant files are included, that the ignored
|
|
||||||
# files are present.
|
|
||||||
self.assertIsInstance(helm_chart, Chart)
|
|
||||||
self.assertTrue(hasattr(helm_chart, 'metadata'))
|
|
||||||
self.assertTrue(hasattr(helm_chart, 'values'))
|
|
||||||
self.assertTrue(hasattr(helm_chart, 'files'))
|
|
||||||
actual_files = sorted(helm_chart.files, key=lambda x: x.value)
|
|
||||||
self.assertEqual(expected_files, repr(actual_files).strip())
|
|
||||||
|
|
||||||
def test_get_helm_chart_with_dependencies(self):
|
|
||||||
# Main chart directory and files.
|
# Main chart directory and files.
|
||||||
chart_dir = self.useFixture(fixtures.TempDir())
|
chart_dir = self.useFixture(fixtures.TempDir())
|
||||||
self.addCleanup(shutil.rmtree, chart_dir.path)
|
self.addCleanup(shutil.rmtree, chart_dir.path)
|
||||||
self._write_temporary_file_contents(
|
self._write_temporary_file_contents(
|
||||||
chart_dir.path, 'Chart.yaml', self.chart_yaml)
|
chart_dir.path, 'Chart.yaml', self.chart_yaml)
|
||||||
ch = yaml.safe_load(self.chart_stream)
|
chart_doc = yaml.safe_load(self.chart_doc_yaml)
|
||||||
ch['data']['source_dir'] = (chart_dir.path, '')
|
chart_doc['data']['source_dir'] = (chart_dir.path, '')
|
||||||
|
|
||||||
# Dependency chart directory and files.
|
# Dependency chart directory and files.
|
||||||
dep_chart_dir = self.useFixture(fixtures.TempDir())
|
dep_chart_dir = self.useFixture(fixtures.TempDir())
|
||||||
self.addCleanup(shutil.rmtree, dep_chart_dir.path)
|
self.addCleanup(shutil.rmtree, dep_chart_dir.path)
|
||||||
self._write_temporary_file_contents(
|
self._write_temporary_file_contents(
|
||||||
dep_chart_dir.path, 'Chart.yaml', self.dependency_chart_yaml)
|
dep_chart_dir.path, 'Chart.yaml', self.dep_chart_yaml)
|
||||||
dep_ch = yaml.safe_load(self.dependency_chart_stream)
|
dep_chart_doc = yaml.safe_load(self.dep_chart_doc_yaml)
|
||||||
dep_ch['data']['source_dir'] = (dep_chart_dir.path, '')
|
dep_chart_doc['data']['source_dir'] = (dep_chart_dir.path, '')
|
||||||
|
|
||||||
main_chart = ch
|
# Add dependency
|
||||||
dependency_chart = dep_ch
|
chart_doc['data']['dependencies'] = [dep_chart_doc]
|
||||||
main_chart['data']['dependencies'] = [dependency_chart]
|
|
||||||
|
|
||||||
chartbuilder = ChartBuilder.from_chart_doc(main_chart)
|
# Mock helm cli call
|
||||||
helm_chart = chartbuilder.get_helm_chart()
|
helm_mock = mock.Mock()
|
||||||
|
helm_mock.show_chart.return_value = yaml.safe_load(self.dep_chart_yaml)
|
||||||
|
ChartBuilder.from_chart_doc(chart_doc, helm_mock)
|
||||||
|
|
||||||
expected_dependency = inspect.cleandoc(
|
expected_symlink_path = Path(
|
||||||
"""
|
chart_dir.path).joinpath('charts').joinpath('dependency-chart')
|
||||||
metadata {
|
self.assertTrue(expected_symlink_path.is_symlink())
|
||||||
name: "dependency-chart"
|
self.assertEqual(
|
||||||
version: "0.1.0"
|
dep_chart_dir.path, str(expected_symlink_path.resolve()))
|
||||||
description: "Another sample Helm chart for Kubernetes"
|
|
||||||
}
|
|
||||||
values {
|
|
||||||
}
|
|
||||||
""").strip()
|
|
||||||
|
|
||||||
expected = inspect.cleandoc(
|
|
||||||
"""
|
|
||||||
metadata {
|
|
||||||
name: "hello-world-chart"
|
|
||||||
version: "0.1.0"
|
|
||||||
description: "A sample Helm chart for Kubernetes"
|
|
||||||
}
|
|
||||||
dependencies {
|
|
||||||
metadata {
|
|
||||||
name: "dependency-chart"
|
|
||||||
version: "0.1.0"
|
|
||||||
description: "Another sample Helm chart for Kubernetes"
|
|
||||||
}
|
|
||||||
values {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
values {
|
|
||||||
}
|
|
||||||
""").strip()
|
|
||||||
|
|
||||||
# Validate the main chart.
|
|
||||||
self.assertIsInstance(helm_chart, Chart)
|
|
||||||
self.assertTrue(hasattr(helm_chart, 'metadata'))
|
|
||||||
self.assertTrue(hasattr(helm_chart, 'values'))
|
|
||||||
self.assertEqual(expected, repr(helm_chart).strip())
|
|
||||||
|
|
||||||
# Validate the dependency chart.
|
|
||||||
self.assertTrue(hasattr(helm_chart, 'dependencies'))
|
|
||||||
self.assertEqual(1, len(helm_chart.dependencies))
|
|
||||||
|
|
||||||
dep_helm_chart = helm_chart.dependencies[0]
|
|
||||||
self.assertIsInstance(dep_helm_chart, Chart)
|
|
||||||
self.assertTrue(hasattr(dep_helm_chart, 'metadata'))
|
|
||||||
self.assertTrue(hasattr(dep_helm_chart, 'values'))
|
|
||||||
self.assertEqual(expected_dependency, repr(dep_helm_chart).strip())
|
|
||||||
|
|
||||||
def test_dump(self):
|
|
||||||
# Validate base case.
|
|
||||||
chart_dir = self.useFixture(fixtures.TempDir())
|
|
||||||
self.addCleanup(shutil.rmtree, chart_dir.path)
|
|
||||||
self._write_temporary_file_contents(
|
|
||||||
chart_dir.path, 'Chart.yaml', self.chart_yaml)
|
|
||||||
ch = yaml.safe_load(self.chart_stream)
|
|
||||||
ch['data']['source_dir'] = (chart_dir.path, '')
|
|
||||||
|
|
||||||
test_chart = ch
|
|
||||||
chartbuilder = ChartBuilder.from_chart_doc(test_chart)
|
|
||||||
self.assertRegex(
|
|
||||||
repr(chartbuilder.dump()),
|
|
||||||
'hello-world-chart.*A sample Helm chart for Kubernetes.*')
|
|
||||||
|
|
||||||
# Validate recursive case (with dependencies).
|
|
||||||
dep_chart_dir = self.useFixture(fixtures.TempDir())
|
|
||||||
self.addCleanup(shutil.rmtree, dep_chart_dir.path)
|
|
||||||
self._write_temporary_file_contents(
|
|
||||||
dep_chart_dir.path, 'Chart.yaml', self.dependency_chart_yaml)
|
|
||||||
dep_ch = yaml.safe_load(self.dependency_chart_stream)
|
|
||||||
dep_ch['data']['source_dir'] = (dep_chart_dir.path, '')
|
|
||||||
|
|
||||||
dependency_chart = dep_ch
|
|
||||||
test_chart['data']['dependencies'] = [dependency_chart]
|
|
||||||
chartbuilder = ChartBuilder.from_chart_doc(test_chart)
|
|
||||||
|
|
||||||
re = inspect.cleandoc(
|
|
||||||
"""
|
|
||||||
hello-world-chart.*A sample Helm chart for Kubernetes.*
|
|
||||||
dependency-chart.*Another sample Helm chart for Kubernetes.*
|
|
||||||
""").replace('\n', '').strip()
|
|
||||||
self.assertRegex(repr(chartbuilder.dump()), re)
|
|
||||||
|
|
||||||
|
|
||||||
class ChartBuilderNegativeTestCase(BaseChartBuilderTestCase):
|
|
||||||
def setUp(self):
|
|
||||||
super(ChartBuilderNegativeTestCase, self).setUp()
|
|
||||||
# Create an exception for testing since instantiating one manually
|
|
||||||
# is tedious.
|
|
||||||
try:
|
|
||||||
str(b'\xff', 'utf8')
|
|
||||||
except UnicodeDecodeError as e:
|
|
||||||
self.exc_to_raise = e
|
|
||||||
else:
|
|
||||||
self.fail('Failed to create an exception needed for testing.')
|
|
||||||
|
|
||||||
def test_get_files_always_fails_to_read_binary_file_raises_exc(self):
|
|
||||||
chart_dir = self.useFixture(fixtures.TempDir())
|
|
||||||
self.addCleanup(shutil.rmtree, chart_dir.path)
|
|
||||||
for filename in ['foo', 'bar', 'Chart.yaml', 'values.yaml']:
|
|
||||||
self._write_temporary_file_contents(
|
|
||||||
chart_dir.path, filename, "DIRC^@^@^@^B^@^@^@×Z®<86>F.1")
|
|
||||||
|
|
||||||
chartbuilder = ChartBuilder.from_chart_doc(
|
|
||||||
self._get_test_chart(chart_dir))
|
|
||||||
|
|
||||||
# Confirm it failed for both encodings.
|
|
||||||
error_re = (
|
|
||||||
r'.*A str exception occurred while trying to read file:'
|
|
||||||
r'.*Details:\n.*\(encoding=utf-8\).*\n\(encoding=latin1\)')
|
|
||||||
with mock.patch("builtins.open", mock.mock_open(read_data="")) \
|
|
||||||
as mock_file:
|
|
||||||
mock_file.return_value.read.side_effect = self.exc_to_raise
|
|
||||||
self.assertRaisesRegexp(
|
|
||||||
chartbuilder_exceptions.FilesLoadException, error_re,
|
|
||||||
chartbuilder.get_files)
|
|
||||||
|
|
||||||
def test_get_files_fails_once_to_read_binary_file_passes(self):
|
|
||||||
chart_dir = self.useFixture(fixtures.TempDir())
|
|
||||||
self.addCleanup(shutil.rmtree, chart_dir.path)
|
|
||||||
files = ['foo', 'bar']
|
|
||||||
for filename in files:
|
|
||||||
self._write_temporary_file_contents(
|
|
||||||
chart_dir.path, filename, "DIRC^@^@^@^B^@^@^@×Z®<86>F.1")
|
|
||||||
|
|
||||||
chartbuilder = ChartBuilder.from_chart_doc(
|
|
||||||
self._get_test_chart(chart_dir))
|
|
||||||
|
|
||||||
side_effects = [self.exc_to_raise, "", ""]
|
|
||||||
with mock.patch("builtins.open", mock.mock_open(read_data="")) \
|
|
||||||
as mock_file:
|
|
||||||
mock_file.return_value.read.side_effect = side_effects
|
|
||||||
chartbuilder.get_files()
|
|
||||||
|
|
|
@ -37,10 +37,10 @@ class LockTestCase(testtools.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(LockTestCase, self).setUp()
|
super(LockTestCase, self).setUp()
|
||||||
self_link = "/apis/armada.tiller/v1/namespaces/default/locks/"\
|
self_link = "/apis/armada.helm/v1/namespaces/default/locks/"\
|
||||||
"locks.armada.tiller.test"
|
"locks.armada.helm.test"
|
||||||
self.resp = {
|
self.resp = {
|
||||||
'apiVersion': "armada.tiller/v1",
|
'apiVersion': "armada.helm/v1",
|
||||||
'data': {
|
'data': {
|
||||||
'lastUpdated': "2019-01-22T16:20:14Z"
|
'lastUpdated': "2019-01-22T16:20:14Z"
|
||||||
},
|
},
|
||||||
|
|
|
@ -12,190 +12,22 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from google.protobuf.any_pb2 import Any
|
import mock
|
||||||
from hapi.chart.chart_pb2 import Chart
|
|
||||||
from hapi.chart.config_pb2 import Config
|
|
||||||
from hapi.chart.metadata_pb2 import Metadata
|
|
||||||
from hapi.chart.template_pb2 import Template
|
|
||||||
|
|
||||||
from armada.handlers.release_diff import ReleaseDiff
|
from armada.handlers.release_diff import ReleaseDiff
|
||||||
from armada.tests.unit import base
|
from armada.tests.unit import base
|
||||||
|
|
||||||
|
|
||||||
# Tests for diffs which can occur in both top-level or dependency charts,
|
|
||||||
# and thus are inherited by both of those test classes.
|
|
||||||
class _BaseReleaseDiffTestCase():
|
|
||||||
def setUp(self):
|
|
||||||
super(base.ArmadaTestCase, self).setUp()
|
|
||||||
self.old_chart = self.make_chart()
|
|
||||||
self.old_values = self.make_values()
|
|
||||||
|
|
||||||
def make_chart(self):
|
|
||||||
chart = self._make_chart()
|
|
||||||
dep = self._make_chart()
|
|
||||||
dep.metadata.name = 'dep1'
|
|
||||||
sub_dep = self._make_chart()
|
|
||||||
sub_dep.metadata.name = 'dep1'
|
|
||||||
sub_sub_dep = self._make_chart()
|
|
||||||
sub_sub_dep.metadata.name = 'dep1'
|
|
||||||
sub_dep.dependencies.extend([sub_sub_dep])
|
|
||||||
dep.dependencies.extend([sub_dep])
|
|
||||||
chart.dependencies.extend([dep])
|
|
||||||
return chart
|
|
||||||
|
|
||||||
def _make_chart(self):
|
|
||||||
return Chart(
|
|
||||||
metadata=Metadata(
|
|
||||||
description='chart description',
|
|
||||||
name='chart_name',
|
|
||||||
version='0.1.2'),
|
|
||||||
templates=[
|
|
||||||
Template(
|
|
||||||
name='template_name', data='template content'.encode())
|
|
||||||
],
|
|
||||||
files=[
|
|
||||||
Any(type_url='./file_name.ext', value='file content'.encode())
|
|
||||||
],
|
|
||||||
dependencies=[],
|
|
||||||
values=Config(raw='{param: d1}'))
|
|
||||||
|
|
||||||
def make_values(self):
|
|
||||||
return {'param': 'o1'}
|
|
||||||
|
|
||||||
def _test_chart_diff(self, update_chart):
|
|
||||||
new_chart = self.make_chart()
|
|
||||||
chart_to_update = self.get_chart_to_update(new_chart)
|
|
||||||
update_chart(chart_to_update)
|
|
||||||
diff = ReleaseDiff(
|
|
||||||
self.old_chart, self.old_values, new_chart,
|
|
||||||
self.old_values).get_diff()
|
|
||||||
self.assertTrue(diff)
|
|
||||||
|
|
||||||
def get_chart_to_update(self, chart):
|
|
||||||
raise NotImplementedError('Implement in subclass')
|
|
||||||
|
|
||||||
def test_metadata_non_name_diff_ignored(self):
|
|
||||||
new_chart = self.make_chart()
|
|
||||||
chart_to_update = self.get_chart_to_update(new_chart)
|
|
||||||
chart_to_update.metadata.description = 'new chart description'
|
|
||||||
diff = ReleaseDiff(
|
|
||||||
self.old_chart, self.old_values, new_chart,
|
|
||||||
self.old_values).get_diff()
|
|
||||||
self.assertFalse(diff)
|
|
||||||
|
|
||||||
def test_metadata_name_diff(self):
|
|
||||||
def update_chart(chart):
|
|
||||||
chart.metadata.name = 'new_chart_name'
|
|
||||||
|
|
||||||
self._test_chart_diff(update_chart)
|
|
||||||
|
|
||||||
def test_default_values_diff(self):
|
|
||||||
def update_chart(chart):
|
|
||||||
chart.values.raw = '{param: d2}'
|
|
||||||
|
|
||||||
self._test_chart_diff(update_chart)
|
|
||||||
|
|
||||||
def test_template_name_diff(self):
|
|
||||||
def update_chart(chart):
|
|
||||||
chart.templates[0].name = 'new_template_name'
|
|
||||||
|
|
||||||
self._test_chart_diff(update_chart)
|
|
||||||
|
|
||||||
def test_template_data_diff(self):
|
|
||||||
def update_chart(chart):
|
|
||||||
chart.templates[0].data = 'new template content'.encode()
|
|
||||||
|
|
||||||
self._test_chart_diff(update_chart)
|
|
||||||
|
|
||||||
def test_add_template_diff(self):
|
|
||||||
def update_chart(chart):
|
|
||||||
chart.templates.extend(
|
|
||||||
[
|
|
||||||
Template(
|
|
||||||
name='new_template_name',
|
|
||||||
data='new template content'.encode())
|
|
||||||
])
|
|
||||||
|
|
||||||
self._test_chart_diff(update_chart)
|
|
||||||
|
|
||||||
def test_remove_template_diff(self):
|
|
||||||
def update_chart(chart):
|
|
||||||
del chart.templates[0]
|
|
||||||
|
|
||||||
self._test_chart_diff(update_chart)
|
|
||||||
|
|
||||||
def test_file_type_url_diff(self):
|
|
||||||
def update_chart(chart):
|
|
||||||
chart.files[0].type_url = './new_file_name.ext'
|
|
||||||
|
|
||||||
self._test_chart_diff(update_chart)
|
|
||||||
|
|
||||||
def test_file_value_diff(self):
|
|
||||||
def update_chart(chart):
|
|
||||||
chart.files[0].value = 'new file content'.encode()
|
|
||||||
|
|
||||||
self._test_chart_diff(update_chart)
|
|
||||||
|
|
||||||
def test_add_file_diff(self):
|
|
||||||
def update_chart(chart):
|
|
||||||
chart.files.extend(
|
|
||||||
[
|
|
||||||
Any(
|
|
||||||
type_url='./new_file_name.ext',
|
|
||||||
value='new file content'.encode())
|
|
||||||
])
|
|
||||||
|
|
||||||
self._test_chart_diff(update_chart)
|
|
||||||
|
|
||||||
def test_remove_file_diff(self):
|
|
||||||
def update_chart(chart):
|
|
||||||
del chart.files[0]
|
|
||||||
|
|
||||||
self._test_chart_diff(update_chart)
|
|
||||||
|
|
||||||
def test_add_dependency_diff(self):
|
|
||||||
def update_chart(chart):
|
|
||||||
dep = self._make_chart()
|
|
||||||
dep.metadata.name = 'dep2'
|
|
||||||
chart.dependencies.extend([dep])
|
|
||||||
|
|
||||||
self._test_chart_diff(update_chart)
|
|
||||||
|
|
||||||
def test_remove_dependency_diff(self):
|
|
||||||
def update_chart(chart):
|
|
||||||
del chart.dependencies[0]
|
|
||||||
|
|
||||||
self._test_chart_diff(update_chart)
|
|
||||||
|
|
||||||
|
|
||||||
# Test diffs (or absence of) in top-level chart / values.
|
# Test diffs (or absence of) in top-level chart / values.
|
||||||
class ReleaseDiffTestCase(_BaseReleaseDiffTestCase, base.ArmadaTestCase):
|
class ReleaseDiffTestCase(base.ArmadaTestCase):
|
||||||
def get_chart_to_update(self, chart):
|
def test_same_input(self):
|
||||||
return chart
|
|
||||||
|
|
||||||
def test_same_input_no_diff(self):
|
|
||||||
diff = ReleaseDiff(
|
diff = ReleaseDiff(
|
||||||
self.old_chart, self.old_values, self.make_chart(),
|
mock.sentinel.chart, mock.sentinel.values, mock.sentinel.chart,
|
||||||
self.make_values()).get_diff()
|
mock.sentinel.values).get_diff()
|
||||||
self.assertFalse(diff)
|
self.assertFalse(diff)
|
||||||
|
|
||||||
def test_override_values_diff(self):
|
def test_diff_input(self):
|
||||||
new_values = {'param': 'o2'}
|
|
||||||
diff = ReleaseDiff(
|
diff = ReleaseDiff(
|
||||||
self.old_chart, self.old_values, self.old_chart,
|
mock.sentinel.old_chart, mock.sentinel.old_values,
|
||||||
new_values).get_diff()
|
mock.sentinel.new_chart, mock.sentinel.new_values).get_diff()
|
||||||
self.assertTrue(diff)
|
self.assertTrue(diff)
|
||||||
|
|
||||||
|
|
||||||
# Test diffs in dependencies.
|
|
||||||
class DependencyReleaseDiffTestCase(_BaseReleaseDiffTestCase,
|
|
||||||
base.ArmadaTestCase):
|
|
||||||
def get_chart_to_update(self, chart):
|
|
||||||
return chart.dependencies[0]
|
|
||||||
|
|
||||||
|
|
||||||
# Test diffs in transitive dependencies.
|
|
||||||
class TransitiveDependencyReleaseDiffTestCase(_BaseReleaseDiffTestCase,
|
|
||||||
base.ArmadaTestCase):
|
|
||||||
def get_chart_to_update(self, chart):
|
|
||||||
return chart.dependencies[0].dependencies[0]
|
|
||||||
|
|
|
@ -15,58 +15,42 @@
|
||||||
import mock
|
import mock
|
||||||
|
|
||||||
from armada import const
|
from armada import const
|
||||||
|
from armada.exceptions.helm_exceptions import HelmCommandException
|
||||||
|
from armada.handlers import helm
|
||||||
from armada.handlers import test
|
from armada.handlers import test
|
||||||
from armada.handlers import tiller
|
|
||||||
from armada.tests.unit import base
|
from armada.tests.unit import base
|
||||||
from armada.tests.test_utils import AttrDict
|
|
||||||
from armada.utils import helm
|
|
||||||
|
|
||||||
|
|
||||||
class TestHandlerTestCase(base.ArmadaTestCase):
|
class TestHandlerTestCase(base.ArmadaTestCase):
|
||||||
def _test_test_release_for_success(self, expected_success, results):
|
def _test_test_release_for_success(self, expected_success, exception):
|
||||||
@mock.patch('armada.handlers.tiller.K8s')
|
@mock.patch('armada.handlers.helm.K8s')
|
||||||
def do_test(_):
|
def do_test(_):
|
||||||
tiller_obj = tiller.Tiller('host', '8080', None)
|
helm_obj = helm.Helm()
|
||||||
release = 'release'
|
release = 'release'
|
||||||
|
|
||||||
tiller_obj.test_release = mock.Mock()
|
helm_obj.test_release = mock.Mock()
|
||||||
tiller_obj.test_release.return_value = AttrDict(
|
if exception:
|
||||||
**{'results': results})
|
helm_obj.test_release.side_effect = exception
|
||||||
|
|
||||||
test_handler = test.Test({}, release, tiller_obj)
|
test_handler = test.Test({}, release, helm_obj)
|
||||||
success = test_handler.test_release_for_success()
|
success = test_handler.test_release_for_success()
|
||||||
|
|
||||||
self.assertEqual(expected_success, success)
|
self.assertEqual(expected_success, success)
|
||||||
|
|
||||||
do_test()
|
do_test()
|
||||||
|
|
||||||
def test_no_results(self):
|
|
||||||
self._test_test_release_for_success(True, [])
|
|
||||||
|
|
||||||
def test_unknown(self):
|
|
||||||
self._test_test_release_for_success(
|
|
||||||
False, [
|
|
||||||
AttrDict(**{'status': helm.TESTRUN_STATUS_SUCCESS}),
|
|
||||||
AttrDict(**{'status': helm.TESTRUN_STATUS_UNKNOWN})
|
|
||||||
])
|
|
||||||
|
|
||||||
def test_success(self):
|
def test_success(self):
|
||||||
self._test_test_release_for_success(
|
self._test_test_release_for_success(True, None)
|
||||||
True, [AttrDict(**{'status': helm.TESTRUN_STATUS_SUCCESS})])
|
|
||||||
|
|
||||||
def test_failure(self):
|
def test_failure(self):
|
||||||
self._test_test_release_for_success(
|
self._test_test_release_for_success(
|
||||||
False, [
|
False, HelmCommandException(mock.Mock()))
|
||||||
AttrDict(**{'status': helm.TESTRUN_STATUS_SUCCESS}),
|
|
||||||
AttrDict(**{'status': helm.TESTRUN_STATUS_FAILURE})
|
|
||||||
])
|
|
||||||
|
|
||||||
def test_running(self):
|
def test_exception(self):
|
||||||
self._test_test_release_for_success(
|
def test():
|
||||||
False, [
|
self._test_test_release_for_success(False, Exception())
|
||||||
AttrDict(**{'status': helm.TESTRUN_STATUS_SUCCESS}),
|
|
||||||
AttrDict(**{'status': helm.TESTRUN_STATUS_RUNNING})
|
self.assertRaises(Exception, test)
|
||||||
])
|
|
||||||
|
|
||||||
def test_cg_disabled(self):
|
def test_cg_disabled(self):
|
||||||
"""Test that tests are disabled when a chart group disables all
|
"""Test that tests are disabled when a chart group disables all
|
||||||
|
@ -74,8 +58,8 @@ class TestHandlerTestCase(base.ArmadaTestCase):
|
||||||
"""
|
"""
|
||||||
test_handler = test.Test(
|
test_handler = test.Test(
|
||||||
chart={},
|
chart={},
|
||||||
release_name='release',
|
release_id=helm.HelmReleaseId('release_ns', 'release'),
|
||||||
tiller=mock.Mock(),
|
helm=mock.Mock(),
|
||||||
cg_test_charts=False)
|
cg_test_charts=False)
|
||||||
|
|
||||||
assert test_handler.test_enabled is False
|
assert test_handler.test_enabled is False
|
||||||
|
@ -86,8 +70,8 @@ class TestHandlerTestCase(base.ArmadaTestCase):
|
||||||
"""
|
"""
|
||||||
test_handler = test.Test(
|
test_handler = test.Test(
|
||||||
chart={'test': True},
|
chart={'test': True},
|
||||||
release_name='release',
|
release_id=helm.HelmReleaseId('release_ns', 'release'),
|
||||||
tiller=mock.Mock(),
|
helm=mock.Mock(),
|
||||||
cg_test_charts=False)
|
cg_test_charts=False)
|
||||||
|
|
||||||
assert test_handler.test_enabled is True
|
assert test_handler.test_enabled is True
|
||||||
|
@ -100,8 +84,8 @@ class TestHandlerTestCase(base.ArmadaTestCase):
|
||||||
chart={'test': {
|
chart={'test': {
|
||||||
'enabled': True
|
'enabled': True
|
||||||
}},
|
}},
|
||||||
release_name='release',
|
release_id=helm.HelmReleaseId('release_ns', 'release'),
|
||||||
tiller=mock.Mock(),
|
helm=mock.Mock(),
|
||||||
cg_test_charts=False)
|
cg_test_charts=False)
|
||||||
|
|
||||||
assert test_handler.test_enabled is True
|
assert test_handler.test_enabled is True
|
||||||
|
@ -112,8 +96,8 @@ class TestHandlerTestCase(base.ArmadaTestCase):
|
||||||
"""
|
"""
|
||||||
test_handler = test.Test(
|
test_handler = test.Test(
|
||||||
chart={'test': False},
|
chart={'test': False},
|
||||||
release_name='release',
|
release_id=helm.HelmReleaseId('release_ns', 'release'),
|
||||||
tiller=mock.Mock(),
|
helm=mock.Mock(),
|
||||||
cg_test_charts=True)
|
cg_test_charts=True)
|
||||||
|
|
||||||
assert test_handler.test_enabled is False
|
assert test_handler.test_enabled is False
|
||||||
|
@ -126,8 +110,8 @@ class TestHandlerTestCase(base.ArmadaTestCase):
|
||||||
chart={'test': {
|
chart={'test': {
|
||||||
'enabled': False
|
'enabled': False
|
||||||
}},
|
}},
|
||||||
release_name='release',
|
release_id=helm.HelmReleaseId('release_ns', 'release'),
|
||||||
tiller=mock.Mock(),
|
helm=mock.Mock(),
|
||||||
cg_test_charts=True)
|
cg_test_charts=True)
|
||||||
|
|
||||||
assert test_handler.test_enabled is False
|
assert test_handler.test_enabled is False
|
||||||
|
@ -138,8 +122,8 @@ class TestHandlerTestCase(base.ArmadaTestCase):
|
||||||
"""
|
"""
|
||||||
test_handler = test.Test(
|
test_handler = test.Test(
|
||||||
chart={},
|
chart={},
|
||||||
release_name='release',
|
release_id=helm.HelmReleaseId('release_ns', 'release'),
|
||||||
tiller=mock.Mock(),
|
helm=mock.Mock(),
|
||||||
cg_test_charts=False,
|
cg_test_charts=False,
|
||||||
enable_all=True)
|
enable_all=True)
|
||||||
|
|
||||||
|
@ -151,8 +135,8 @@ class TestHandlerTestCase(base.ArmadaTestCase):
|
||||||
"""
|
"""
|
||||||
test_handler = test.Test(
|
test_handler = test.Test(
|
||||||
chart={'test': True},
|
chart={'test': True},
|
||||||
release_name='release',
|
release_id=helm.HelmReleaseId('release_ns', 'release'),
|
||||||
tiller=mock.Mock(),
|
helm=mock.Mock(),
|
||||||
enable_all=True)
|
enable_all=True)
|
||||||
|
|
||||||
assert test_handler.test_enabled is True
|
assert test_handler.test_enabled is True
|
||||||
|
@ -165,8 +149,8 @@ class TestHandlerTestCase(base.ArmadaTestCase):
|
||||||
chart={'test': {
|
chart={'test': {
|
||||||
'enabled': False
|
'enabled': False
|
||||||
}},
|
}},
|
||||||
release_name='release',
|
release_id=helm.HelmReleaseId('release_ns', 'release'),
|
||||||
tiller=mock.Mock(),
|
helm=mock.Mock(),
|
||||||
enable_all=True)
|
enable_all=True)
|
||||||
|
|
||||||
assert test_handler.test_enabled is True
|
assert test_handler.test_enabled is True
|
||||||
|
@ -176,27 +160,21 @@ class TestHandlerTestCase(base.ArmadaTestCase):
|
||||||
for a chart's test key.
|
for a chart's test key.
|
||||||
"""
|
"""
|
||||||
test_handler = test.Test(
|
test_handler = test.Test(
|
||||||
chart={'test': False}, release_name='release', tiller=mock.Mock())
|
chart={'test': False},
|
||||||
|
release_id=helm.HelmReleaseId('release_ns', 'release'),
|
||||||
|
helm=mock.Mock())
|
||||||
|
|
||||||
assert not test_handler.test_enabled
|
assert not test_handler.test_enabled
|
||||||
|
|
||||||
def test_deprecated_test_key_true(self):
|
|
||||||
"""Test that cleanup is enabled by default when tests are enabled using
|
|
||||||
the deprecated, boolean value for a chart's `test` key.
|
|
||||||
"""
|
|
||||||
test_handler = test.Test(
|
|
||||||
chart={'test': True}, release_name='release', tiller=mock.Mock())
|
|
||||||
|
|
||||||
assert test_handler.test_enabled is True
|
|
||||||
assert test_handler.cleanup is True
|
|
||||||
|
|
||||||
def test_deprecated_test_key_timeout(self):
|
def test_deprecated_test_key_timeout(self):
|
||||||
"""Test that the default Tiller timeout is used when tests are enabled
|
"""Test that the default helm timeout is used when tests are enabled
|
||||||
using the deprecated, boolean value for a chart's `test` key.
|
using the deprecated, boolean value for a chart's `test` key.
|
||||||
"""
|
"""
|
||||||
mock_tiller = mock.Mock()
|
mock_helm = mock.Mock()
|
||||||
test_handler = test.Test(
|
test_handler = test.Test(
|
||||||
chart={'test': True}, release_name='release', tiller=mock_tiller)
|
chart={'test': True},
|
||||||
|
release_id=helm.HelmReleaseId('release_ns', 'release'),
|
||||||
|
helm=mock_helm)
|
||||||
|
|
||||||
assert test_handler.timeout == const.DEFAULT_TEST_TIMEOUT
|
assert test_handler.timeout == const.DEFAULT_TEST_TIMEOUT
|
||||||
|
|
||||||
|
@ -208,86 +186,21 @@ class TestHandlerTestCase(base.ArmadaTestCase):
|
||||||
chart={'test': {
|
chart={'test': {
|
||||||
'enabled': False
|
'enabled': False
|
||||||
}},
|
}},
|
||||||
release_name='release',
|
release_id=helm.HelmReleaseId('release_ns', 'release'),
|
||||||
tiller=mock.Mock())
|
helm=mock.Mock())
|
||||||
|
|
||||||
assert test_handler.test_enabled is False
|
assert test_handler.test_enabled is False
|
||||||
|
|
||||||
def test_tests_enabled(self):
|
|
||||||
"""Test that cleanup is disabled (by default) when tests are enabled by
|
|
||||||
a chart's values using the `test.enabled` path.
|
|
||||||
"""
|
|
||||||
test_handler = test.Test(
|
|
||||||
chart={'test': {
|
|
||||||
'enabled': True
|
|
||||||
}},
|
|
||||||
release_name='release',
|
|
||||||
tiller=mock.Mock())
|
|
||||||
|
|
||||||
assert test_handler.test_enabled is True
|
|
||||||
assert test_handler.cleanup is False
|
|
||||||
|
|
||||||
def test_tests_enabled_cleanup_enabled(self):
|
|
||||||
"""Test that the test handler uses the values provided by a chart's
|
|
||||||
`test` key.
|
|
||||||
"""
|
|
||||||
test_handler = test.Test(
|
|
||||||
chart={'test': {
|
|
||||||
'enabled': True,
|
|
||||||
'options': {
|
|
||||||
'cleanup': True
|
|
||||||
}
|
|
||||||
}},
|
|
||||||
release_name='release',
|
|
||||||
tiller=mock.Mock())
|
|
||||||
|
|
||||||
assert test_handler.test_enabled is True
|
|
||||||
assert test_handler.cleanup is True
|
|
||||||
|
|
||||||
def test_tests_enabled_cleanup_disabled(self):
|
|
||||||
"""Test that the test handler uses the values provided by a chart's
|
|
||||||
`test` key.
|
|
||||||
"""
|
|
||||||
test_handler = test.Test(
|
|
||||||
chart={'test': {
|
|
||||||
'enabled': True,
|
|
||||||
'options': {
|
|
||||||
'cleanup': False
|
|
||||||
}
|
|
||||||
}},
|
|
||||||
release_name='release',
|
|
||||||
tiller=mock.Mock())
|
|
||||||
|
|
||||||
assert test_handler.test_enabled is True
|
|
||||||
assert test_handler.cleanup is False
|
|
||||||
|
|
||||||
def test_no_test_values(self):
|
def test_no_test_values(self):
|
||||||
"""Test that the default values are enforced when no chart `test`
|
"""Test that the default values are enforced when no chart `test`
|
||||||
values are provided (i.e. tests are enabled and cleanup is disabled).
|
values are provided (i.e. tests are enabled).
|
||||||
"""
|
"""
|
||||||
test_handler = test.Test(
|
test_handler = test.Test(
|
||||||
chart={}, release_name='release', tiller=mock.Mock())
|
chart={},
|
||||||
|
release_id=helm.HelmReleaseId('release_ns', 'release'),
|
||||||
|
helm=mock.Mock())
|
||||||
|
|
||||||
assert test_handler.test_enabled is True
|
assert test_handler.test_enabled is True
|
||||||
assert test_handler.cleanup is False
|
|
||||||
|
|
||||||
def test_override_cleanup(self):
|
|
||||||
"""Test that a cleanup value passed to the Test handler (i.e. from the
|
|
||||||
API/CLI) takes precedence over a chart's `test.cleanup` value.
|
|
||||||
"""
|
|
||||||
test_handler = test.Test(
|
|
||||||
chart={'test': {
|
|
||||||
'enabled': True,
|
|
||||||
'options': {
|
|
||||||
'cleanup': False
|
|
||||||
}
|
|
||||||
}},
|
|
||||||
release_name='release',
|
|
||||||
tiller=mock.Mock(),
|
|
||||||
cleanup=True)
|
|
||||||
|
|
||||||
assert test_handler.test_enabled is True
|
|
||||||
assert test_handler.cleanup is True
|
|
||||||
|
|
||||||
def test_default_timeout_value(self):
|
def test_default_timeout_value(self):
|
||||||
"""Test that the default timeout value is used if a test timeout value,
|
"""Test that the default timeout value is used if a test timeout value,
|
||||||
|
@ -297,11 +210,10 @@ class TestHandlerTestCase(base.ArmadaTestCase):
|
||||||
chart={'test': {
|
chart={'test': {
|
||||||
'enabled': True
|
'enabled': True
|
||||||
}},
|
}},
|
||||||
release_name='release',
|
release_id=helm.HelmReleaseId('release_ns', 'release'),
|
||||||
tiller=mock.Mock(),
|
helm=mock.Mock())
|
||||||
cleanup=True)
|
|
||||||
|
|
||||||
assert test_handler.timeout == const.DEFAULT_TILLER_TIMEOUT
|
assert test_handler.timeout == helm.DEFAULT_HELM_TIMEOUT
|
||||||
|
|
||||||
def test_timeout_value(self):
|
def test_timeout_value(self):
|
||||||
"""Test that a chart's test timeout value, `test.timeout` overrides the
|
"""Test that a chart's test timeout value, `test.timeout` overrides the
|
||||||
|
@ -311,8 +223,7 @@ class TestHandlerTestCase(base.ArmadaTestCase):
|
||||||
|
|
||||||
test_handler = test.Test(
|
test_handler = test.Test(
|
||||||
chart=chart,
|
chart=chart,
|
||||||
release_name='release',
|
release_id=helm.HelmReleaseId('release_ns', 'release'),
|
||||||
tiller=mock.Mock(),
|
helm=mock.Mock())
|
||||||
cleanup=True)
|
|
||||||
|
|
||||||
assert test_handler.timeout is chart['test']['timeout']
|
assert test_handler.timeout is chart['test']['timeout']
|
||||||
|
|
|
@ -16,6 +16,7 @@ import mock
|
||||||
|
|
||||||
from armada import const
|
from armada import const
|
||||||
from armada.exceptions import manifest_exceptions
|
from armada.exceptions import manifest_exceptions
|
||||||
|
from armada.handlers import helm
|
||||||
from armada.handlers import wait
|
from armada.handlers import wait
|
||||||
from armada.tests.unit import base
|
from armada.tests.unit import base
|
||||||
|
|
||||||
|
@ -33,9 +34,8 @@ class ChartWaitTestCase(base.ArmadaTestCase):
|
||||||
}
|
}
|
||||||
return wait.ChartWait(
|
return wait.ChartWait(
|
||||||
k8s=mock.MagicMock(),
|
k8s=mock.MagicMock(),
|
||||||
release_name='test-test',
|
release_id=helm.HelmReleaseId('test', 'test-test'),
|
||||||
chart=chart,
|
chart=chart,
|
||||||
namespace='test',
|
|
||||||
k8s_wait_attempts=1,
|
k8s_wait_attempts=1,
|
||||||
k8s_wait_attempt_sleep=1,
|
k8s_wait_attempt_sleep=1,
|
||||||
timeout=timeout)
|
timeout=timeout)
|
||||||
|
@ -215,10 +215,11 @@ class PodWaitTestCase(base.ArmadaTestCase):
|
||||||
test_pods = [
|
test_pods = [
|
||||||
mock_resource({
|
mock_resource({
|
||||||
'key': 'value',
|
'key': 'value',
|
||||||
'helm.sh/hook': 'test-success'
|
'helm.sh/hook': 'test'
|
||||||
}),
|
}),
|
||||||
|
mock_resource({'helm.sh/hook': 'test-success'}),
|
||||||
mock_resource({'helm.sh/hook': 'test-failure'}),
|
mock_resource({'helm.sh/hook': 'test-failure'}),
|
||||||
mock_resource({'helm.sh/hook': 'test-success,pre-install'}),
|
mock_resource({'helm.sh/hook': 'test,pre-install'}),
|
||||||
]
|
]
|
||||||
job_pods = [
|
job_pods = [
|
||||||
mock_resource(owner_references=[mock.Mock(kind='Job')]),
|
mock_resource(owner_references=[mock.Mock(kind='Job')]),
|
||||||
|
|
|
@ -18,7 +18,10 @@ TESTRUN_STATUS_FAILURE = 2
|
||||||
TESTRUN_STATUS_RUNNING = 3
|
TESTRUN_STATUS_RUNNING = 3
|
||||||
|
|
||||||
HELM_HOOK_ANNOTATION = 'helm.sh/hook'
|
HELM_HOOK_ANNOTATION = 'helm.sh/hook'
|
||||||
HELM_TEST_HOOKS = ['test-success', 'test-failure']
|
# TODO: Eventually remove 'test-failure' as it is removed in Helm 3,
|
||||||
|
# leaving for now to ensure test runs leftover from Helm 2 get deleted
|
||||||
|
# and don't cause name conflicts.
|
||||||
|
HELM_TEST_HOOKS = ['test', 'test-success', 'test-failure']
|
||||||
|
|
||||||
|
|
||||||
def is_test_pod(pod):
|
def is_test_pod(pod):
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from armada.utils.helm import get_test_suite_run_success
|
import dateutil
|
||||||
|
|
||||||
|
|
||||||
def release_prefixer(prefix, release):
|
def release_prefixer(prefix, release):
|
||||||
|
@ -35,26 +35,24 @@ def label_selectors(labels):
|
||||||
|
|
||||||
def get_release_status(release):
|
def get_release_status(release):
|
||||||
"""
|
"""
|
||||||
:param release: protobuf release object
|
:param release: helm release metadata
|
||||||
|
|
||||||
:return: status name of release
|
:return: status name of release
|
||||||
"""
|
"""
|
||||||
|
|
||||||
status = release.info.status
|
return release['info']['status']
|
||||||
return status.Code.Name(status.code)
|
|
||||||
|
|
||||||
|
|
||||||
def get_last_test_result(release):
|
def get_last_test_result(release):
|
||||||
"""
|
"""
|
||||||
:param release: protobuf release object
|
:param release: helm release metadata
|
||||||
|
|
||||||
:return: status name of release
|
:return: whether tests are successful (no tests defined implies success)
|
||||||
"""
|
"""
|
||||||
|
test_hooks = (
|
||||||
status = release.info.status
|
hook for hook in release.get('hooks', []) if any(
|
||||||
if not status.HasField('last_test_suite_run'):
|
e in ['test', 'test-success'] for e in hook['events']))
|
||||||
return None
|
return all(test['last_run']['phase'] == 'Succeeded' for test in test_hooks)
|
||||||
return get_test_suite_run_success(status.last_test_suite_run)
|
|
||||||
|
|
||||||
|
|
||||||
def get_last_deployment_age(release):
|
def get_last_deployment_age(release):
|
||||||
|
@ -64,7 +62,8 @@ def get_last_deployment_age(release):
|
||||||
:return: age in seconds of last deployment of release
|
:return: age in seconds of last deployment of release
|
||||||
"""
|
"""
|
||||||
|
|
||||||
last_deployed = release.info.last_deployed.seconds
|
last_deployed_str = release['info']['last_deployed']
|
||||||
|
last_deployed = dateutil.parser.isoparse(last_deployed_str).timestamp()
|
||||||
now = int(time.time())
|
now = int(time.time())
|
||||||
last_deployment_age = now - last_deployed
|
last_deployment_age = now - last_deployed
|
||||||
|
|
||||||
|
|
|
@ -200,7 +200,7 @@ conf:
|
||||||
'armada:test_manifest': 'rule:admin_required'
|
'armada:test_manifest': 'rule:admin_required'
|
||||||
'armada:test_release': 'rule:admin_required'
|
'armada:test_release': 'rule:admin_required'
|
||||||
'armada:validate_manifest': 'rule:admin_viewer'
|
'armada:validate_manifest': 'rule:admin_viewer'
|
||||||
'tiller:get_release': 'rule:admin_viewer'
|
'armada:get_release': 'rule:admin_viewer'
|
||||||
'tiller:get_status': 'rule:admin_viewer'
|
'tiller:get_status': 'rule:admin_viewer'
|
||||||
tiller:
|
tiller:
|
||||||
# If set to false then some form of Tiller needs to be provided
|
# If set to false then some form of Tiller needs to be provided
|
||||||
|
|
|
@ -328,8 +328,6 @@ Upgrade options to pass through directly to helm.
|
||||||
+---------------+----------+---------------------------------------------------------------+
|
+---------------+----------+---------------------------------------------------------------+
|
||||||
| force | boolean | Same as Helm CLI. |
|
| force | boolean | Same as Helm CLI. |
|
||||||
+---------------+----------+---------------------------------------------------------------+
|
+---------------+----------+---------------------------------------------------------------+
|
||||||
| recreate_pods | boolean | Same as Helm CLI. |
|
|
||||||
+---------------+----------+---------------------------------------------------------------+
|
|
||||||
|
|
||||||
Upgrade - Pre
|
Upgrade - Pre
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
|
|
@ -19,27 +19,7 @@ Chartbuilder Exceptions
|
||||||
|
|
||||||
.. currentmodule:: armada.exceptions.chartbuilder_exceptions
|
.. currentmodule:: armada.exceptions.chartbuilder_exceptions
|
||||||
|
|
||||||
.. autoexception:: DependencyException
|
|
||||||
:members:
|
|
||||||
:show-inheritance:
|
|
||||||
:undoc-members:
|
|
||||||
|
|
||||||
.. autoexception:: FilesLoadException
|
|
||||||
:members:
|
|
||||||
:show-inheritance:
|
|
||||||
:undoc-members:
|
|
||||||
|
|
||||||
.. autoexception:: HelmChartBuildException
|
.. autoexception:: HelmChartBuildException
|
||||||
:members:
|
:members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
|
|
||||||
.. autoexception:: IgnoredFilesLoadException
|
|
||||||
:members:
|
|
||||||
:show-inheritance:
|
|
||||||
:undoc-members:
|
|
||||||
|
|
||||||
.. autoexception:: MetadataLoadException
|
|
||||||
:members:
|
|
||||||
:show-inheritance:
|
|
||||||
:undoc-members:
|
|
||||||
|
|
|
@ -26,10 +26,10 @@
|
||||||
# POST /api/v1.0/tests/
|
# POST /api/v1.0/tests/
|
||||||
#"armada:test_manifest": "rule:admin_required"
|
#"armada:test_manifest": "rule:admin_required"
|
||||||
|
|
||||||
|
# Get helm releases
|
||||||
|
# GET /api/v1.0/releases/
|
||||||
|
#"armada:get_release": "rule:admin_viewer"
|
||||||
|
|
||||||
# Get Tiller status
|
# Get Tiller status
|
||||||
# GET /api/v1.0/status/
|
# GET /api/v1.0/status/
|
||||||
#"tiller:get_status": "rule:admin_viewer"
|
#"tiller:get_status": "rule:admin_viewer"
|
||||||
|
|
||||||
# Get Tiller release
|
|
||||||
# GET /api/v1.0/releases/
|
|
||||||
#"tiller:get_release": "rule:admin_viewer"
|
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
---
|
||||||
|
schema: armada/Chart/v1
|
||||||
|
metadata:
|
||||||
|
schema: metadata/Document/v1
|
||||||
|
name: helm-toolkit
|
||||||
|
data:
|
||||||
|
chart_name: helm-toolkit
|
||||||
|
release: helm-toolkit
|
||||||
|
namespace: helm-tookit
|
||||||
|
install:
|
||||||
|
no_hooks: false
|
||||||
|
upgrade:
|
||||||
|
no_hooks: false
|
||||||
|
values: {}
|
||||||
|
source:
|
||||||
|
type: tar
|
||||||
|
location: https://tarballs.opendev.org/openstack/openstack-helm-infra/helm-toolkit-0.2.23.tgz
|
||||||
|
subpath: helm-toolkit
|
||||||
|
---
|
||||||
|
schema: armada/Chart/v1
|
||||||
|
metadata:
|
||||||
|
schema: metadata/Document/v1
|
||||||
|
name: podinfo
|
||||||
|
data:
|
||||||
|
chart_name: podinfo
|
||||||
|
release: podinfo
|
||||||
|
namespace: podinfo
|
||||||
|
install:
|
||||||
|
no_hooks: false
|
||||||
|
upgrade:
|
||||||
|
no_hooks: false
|
||||||
|
values:
|
||||||
|
foo: bar
|
||||||
|
dependencies:
|
||||||
|
- helm-toolkit
|
||||||
|
source:
|
||||||
|
type: local
|
||||||
|
location: /podinfo/charts/podinfo
|
||||||
|
subpath: .
|
||||||
|
reference: master
|
||||||
|
---
|
||||||
|
schema: armada/ChartGroup/v1
|
||||||
|
metadata:
|
||||||
|
schema: metadata/Document/v1
|
||||||
|
name: podinfo
|
||||||
|
data:
|
||||||
|
description: Deploys Simple Service
|
||||||
|
sequenced: False
|
||||||
|
chart_group:
|
||||||
|
- podinfo
|
||||||
|
---
|
||||||
|
schema: armada/Manifest/v1
|
||||||
|
metadata:
|
||||||
|
schema: metadata/Document/v1
|
||||||
|
name: podinfo
|
||||||
|
data:
|
||||||
|
release_prefix: podinfo
|
||||||
|
chart_groups:
|
||||||
|
- podinfo
|
Loading…
Reference in New Issue