diff --git a/.zuul.yaml b/.zuul.yaml index 4209c8ac..7816d4d9 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -28,6 +28,7 @@ - armada-docker-build-gate-ubuntu_xenial - armada-docker-build-gate-opensuse - armada-airskiff-deploy + - armada-airship2-integration gate: jobs: - openstack-tox-pep8 @@ -126,6 +127,26 @@ - ^releasenotes/.*$ - ^swagger/.*$ +- job: + name: armada-airship2-integration + nodeset: armada-single-node + description: | + Deploy basic airship2 integration example using submitted Armada changes. + timeout: 9600 + voting: false + pre-run: + - tools/gate/playbooks/git-config.yaml + run: tools/gate/playbooks/airship2-integration.yaml + post-run: tools/gate/playbooks/debug-report.yaml + required-projects: + - airship/treasuremap + irrelevant-files: + - ^.*\.rst$ + - ^doc/.*$ + - ^examples/.*$ + - ^releasenotes/.*$ + - ^swagger/.*$ + - job: name: armada-docker-publish-ubuntu_bionic timeout: 1800 diff --git a/armada/cli/apply_chart.py b/armada/cli/apply_chart.py new file mode 100644 index 00000000..ff5af09c --- /dev/null +++ b/armada/cli/apply_chart.py @@ -0,0 +1,207 @@ +# Copyright 2020 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 click +from oslo_config import cfg +import prometheus_client +import yaml + +from armada.cli import CliAction +from armada.exceptions.source_exceptions import InvalidPathException +from armada.handlers import metrics +from armada.handlers.chart_deploy import ChartDeploy +from armada.handlers.chart_download import ChartDownload +from armada.handlers.document import ReferenceResolver +from armada.handlers.lock import lock_and_thread +from armada.handlers.manifest import Chart +from armada.handlers.tiller import Tiller + +CONF = cfg.CONF + + +@click.group() +def apply_chart(): + """ Apply chart to cluster + + """ + + +DESC = """ +This command installs and updates an Armada chart. + +[LOCATION] must be a relative path to Armada Chart or a reference +to an Armada Chart kubernetes CR which has the same format, except as +noted in the v2 document authoring documentation. + +To install or upgrade a chart, run: + + \b + $ armada apply_chart --release-prefix=armada my-chart.yaml + $ armada apply_chart --release-prefix=armada \ + kube:armadacharts/my-namespace/my-chart +""" + +SHORT_DESC = "Command deploys a chart." + + +@apply_chart.command(name='apply_chart', help=DESC, short_help=SHORT_DESC) +@click.argument('location') +@click.option( + '--release-prefix', + help="Prefix to prepend to chart release name.", + required=True) +@click.option( + '--disable-update-post', + help="Disable post-update Tiller operations.", + is_flag=True) +@click.option( + '--disable-update-pre', + help="Disable pre-update Tiller operations.", + is_flag=True) +@click.option( + '--metrics-output', + help=( + "Output path for prometheus metric data, should end in .prom. By " + "default, no metric data is output."), + default=None) +@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( + '--timeout', + help="Specifies time to wait for each chart to fully " + "finish deploying.", + type=int) +@click.option( + '--wait', + help=( + "Force Tiller to wait until the chart is deployed, " + "rather than using the chart's specified wait policy. " + "This is equivalent to sequenced chartgroups."), + is_flag=True) +@click.option( + '--target-chart', + help=( + "The target chart to deploy. Required for specifying " + "which chart to deploy when multiple are available."), + default=None) +@click.option('--bearer-token', help="User Bearer token", default=None) +@click.option('--debug', help="Enable debug logging.", is_flag=True) +@click.pass_context +def apply_chart( + ctx, location, release_prefix, disable_update_post, disable_update_pre, + metrics_output, tiller_host, tiller_port, tiller_namespace, timeout, + wait, target_chart, bearer_token, debug): + CONF.debug = debug + ApplyChart( + ctx, location, release_prefix, disable_update_post, disable_update_pre, + metrics_output, tiller_host, tiller_port, tiller_namespace, timeout, + wait, target_chart, bearer_token).safe_invoke() + + +class ApplyChart(CliAction): + def __init__( + self, ctx, location, release_prefix, disable_update_post, + disable_update_pre, metrics_output, tiller_host, tiller_port, + tiller_namespace, timeout, wait, target_chart, bearer_token): + super(ApplyChart, self).__init__() + self.ctx = ctx + self.release_prefix = release_prefix + # Filename can also be a URL reference + self.location = location + self.disable_update_post = disable_update_post + self.disable_update_pre = disable_update_pre + self.metrics_output = metrics_output + self.tiller_host = tiller_host + self.tiller_port = tiller_port + self.tiller_namespace = tiller_namespace + self.timeout = timeout + self.target_chart = target_chart + self.bearer_token = bearer_token + + def output(self, resp): + for result in resp: + if not resp[result] and not result == 'diff': + self.logger.info('Did not perform chart %s(s)', result) + elif result == 'diff' and not resp[result]: + self.logger.info('No release changes detected') + + ch = resp[result] + if not result == 'diff': + msg = 'Chart {} took action: {}'.format(ch, result) + if result == 'protected': + msg += ' and requires operator attention.' + elif result == 'purge': + msg += ' before install/upgrade.' + self.logger.info(msg) + else: + self.logger.info('Chart/values diff: %s', ch) + + def invoke(self): + with Tiller(tiller_host=self.tiller_host, tiller_port=self.tiller_port, + tiller_namespace=self.tiller_namespace, + bearer_token=self.bearer_token) as tiller: + + try: + doc_data = ReferenceResolver.resolve_reference( + self.location, k8s=tiller.k8s) + documents = list() + for d in doc_data: + documents.extend(list(yaml.safe_load_all(d.decode()))) + except InvalidPathException as ex: + self.logger.error(str(ex)) + return + except yaml.YAMLError as yex: + self.logger.error("Invalid YAML found: %s" % str(yex)) + return + + try: + resp = self.handle(documents, tiller) + self.output(resp) + finally: + if self.metrics_output: + path = self.metrics_output + self.logger.info( + 'Storing metrics output in path: {}'.format(path)) + prometheus_client.write_to_textfile(path, metrics.REGISTRY) + + def handle(self, documents, tiller): + chart = Chart(documents, target_chart=self.target_chart).get_chart() + + lock_name = 'chart-{}'.format(chart['metadata']['name']) + + @lock_and_thread(lock_name) + def _handle(): + chart_download = ChartDownload() + try: + chart_download.get_chart(chart) + chart_deploy = ChartDeploy( + None, self.disable_update_pre, self.disable_update_post, 1, + 1, self.timeout, tiller) + + # TODO: Only get release with matching name. + known_releases = tiller.list_releases() + + return chart_deploy.execute( + chart, None, self.release_prefix, known_releases, 1) + finally: + chart_download.cleanup() + + return _handle() diff --git a/armada/handlers/armada.py b/armada/handlers/armada.py index 68af09ab..6506a49f 100644 --- a/armada/handlers/armada.py +++ b/armada/handlers/armada.py @@ -21,15 +21,14 @@ from armada import const from armada.conf import set_current_chart from armada.exceptions import armada_exceptions from armada.exceptions import override_exceptions -from armada.exceptions import source_exceptions from armada.exceptions import tiller_exceptions from armada.exceptions import validate_exceptions from armada.handlers import metrics from armada.handlers.chart_deploy import ChartDeploy +from armada.handlers.chart_download import ChartDownload from armada.handlers.manifest import Manifest from armada.handlers.override import Override from armada.utils.release import release_prefixer -from armada.utils import source LOG = logging.getLogger(__name__) CONF = cfg.CONF @@ -88,7 +87,7 @@ class Armada(object): raise self.manifest = Manifest( self.documents, target_manifest=target_manifest).get_manifest() - self.chart_cache = {} + self.chart_download = ChartDownload() self.chart_deploy = ChartDeploy( self.manifest, disable_update_pre, disable_update_post, k8s_wait_attempts, k8s_wait_attempt_sleep, timeout, self.tiller) @@ -108,71 +107,7 @@ class Armada(object): for group in manifest_data.get(const.KEYWORD_GROUPS, []): for ch in group.get(const.KEYWORD_DATA).get(const.KEYWORD_CHARTS, []): - self.get_chart(ch) - - def get_chart(self, ch): - manifest_name = self.manifest['metadata']['name'] - chart_name = ch['metadata']['name'] - with metrics.CHART_DOWNLOAD.get_context(manifest_name, chart_name): - return self._get_chart(ch) - - def _get_chart(self, ch): - chart = ch.get(const.KEYWORD_DATA) - chart_source = chart.get('source', {}) - location = chart_source.get('location') - ct_type = chart_source.get('type') - subpath = chart_source.get('subpath', '.') - proxy_server = chart_source.get('proxy_server') - - if ct_type == 'local': - chart['source_dir'] = (location, subpath) - elif ct_type == 'tar': - source_key = (ct_type, location) - - if source_key not in self.chart_cache: - LOG.info( - "Downloading tarball from: %s / proxy %s", location, - proxy_server or "not set") - - if not CONF.certs: - LOG.warn( - 'Disabling server validation certs to extract charts') - tarball_dir = source.get_tarball( - location, verify=False, proxy_server=proxy_server) - else: - tarball_dir = source.get_tarball( - location, verify=CONF.certs, proxy_server=proxy_server) - self.chart_cache[source_key] = tarball_dir - chart['source_dir'] = (self.chart_cache.get(source_key), subpath) - elif ct_type == 'git': - reference = chart_source.get('reference', 'master') - source_key = (ct_type, location, reference) - - if source_key not in self.chart_cache: - auth_method = chart_source.get('auth_method') - - logstr = 'Cloning repo: {} from branch: {}'.format( - location, reference) - if proxy_server: - logstr += ' proxy: {}'.format(proxy_server) - if auth_method: - logstr += ' auth method: {}'.format(auth_method) - LOG.info(logstr) - - repo_dir = source.git_clone( - location, - reference, - proxy_server=proxy_server, - auth_method=auth_method) - - self.chart_cache[source_key] = repo_dir - chart['source_dir'] = (self.chart_cache.get(source_key), subpath) - else: - name = ch['metadata']['name'] - raise source_exceptions.ChartSourceException(ct_type, name) - - for dep in ch.get(const.KEYWORD_DATA, {}).get('dependencies', []): - self.get_chart(dep) + self.chart_download.get_chart(ch, manifest=self.manifest) def sync(self): ''' @@ -285,10 +220,7 @@ class Armada(object): ''' LOG.info("Performing post-flight operations.") - # Delete temp dirs used for deployment - for chart_dir in self.chart_cache.values(): - LOG.debug('Removing temp chart directory: %s', chart_dir) - source.source_cleanup(chart_dir) + self.chart_download.cleanup() def _chart_cleanup(self, prefix, chart_groups, msg): LOG.info('Processing chart cleanup to remove unspecified releases.') diff --git a/armada/handlers/chart_deploy.py b/armada/handlers/chart_deploy.py index acc41d0f..8821bfce 100644 --- a/armada/handlers/chart_deploy.py +++ b/armada/handlers/chart_deploy.py @@ -47,14 +47,16 @@ class ChartDeploy(object): def execute( self, ch, cg_test_all_charts, prefix, known_releases, concurrency): chart_name = ch['metadata']['name'] - manifest_name = self.manifest['metadata']['name'] + manifest_name = self.manifest['metadata'][ + 'name'] if self.manifest else '' with metrics.CHART_HANDLE.get_context(concurrency, manifest_name, chart_name): return self._execute( ch, cg_test_all_charts, prefix, known_releases) def _execute(self, ch, cg_test_all_charts, prefix, known_releases): - manifest_name = self.manifest['metadata']['name'] + manifest_name = self.manifest['metadata'][ + 'name'] if self.manifest else '' chart = ch[const.KEYWORD_DATA] chart_name = ch['metadata']['name'] namespace = chart.get('namespace') diff --git a/armada/handlers/chart_download.py b/armada/handlers/chart_download.py new file mode 100644 index 00000000..6f0cbd1c --- /dev/null +++ b/armada/handlers/chart_download.py @@ -0,0 +1,104 @@ +# Copyright 2020 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 oslo_config import cfg +from oslo_log import log as logging + +from armada import const +from armada.exceptions import source_exceptions +from armada.handlers import metrics +from armada.utils import source + +LOG = logging.getLogger(__name__) +CONF = cfg.CONF + + +class ChartDownload(): + def __init__(self): + self.chart_cache = {} + + def get_chart(self, ch, manifest=None): + manifest_name = manifest['metadata']['name'] if manifest else None + chart_name = ch['metadata']['name'] + with metrics.CHART_DOWNLOAD.get_context(manifest_name, chart_name): + return self._get_chart(ch, manifest) + + def _get_chart(self, ch, manifest): + chart = ch.get(const.KEYWORD_DATA) + chart_source = chart.get('source', {}) + location = chart_source.get('location') + ct_type = chart_source.get('type') + subpath = chart_source.get('subpath', '.') + proxy_server = chart_source.get('proxy_server') + + if ct_type == 'local': + chart['source_dir'] = (location, subpath) + elif ct_type == 'tar': + source_key = (ct_type, location) + + if source_key not in self.chart_cache: + LOG.info( + "Downloading tarball from: %s / proxy %s", location, + proxy_server or "not set") + + if not CONF.certs: + LOG.warn( + 'Disabling server validation certs to extract charts') + tarball_dir = source.get_tarball( + location, verify=False, proxy_server=proxy_server) + else: + tarball_dir = source.get_tarball( + location, verify=CONF.certs, proxy_server=proxy_server) + self.chart_cache[source_key] = tarball_dir + chart['source_dir'] = (self.chart_cache.get(source_key), subpath) + elif ct_type == 'git': + reference = chart_source.get('reference', 'master') + source_key = (ct_type, location, reference) + + if source_key not in self.chart_cache: + auth_method = chart_source.get('auth_method') + + logstr = 'Cloning repo: {} from branch: {}'.format( + location, reference) + if proxy_server: + logstr += ' proxy: {}'.format(proxy_server) + if auth_method: + logstr += ' auth method: {}'.format(auth_method) + LOG.info(logstr) + + repo_dir = source.git_clone( + location, + reference, + proxy_server=proxy_server, + auth_method=auth_method) + + self.chart_cache[source_key] = repo_dir + chart['source_dir'] = (self.chart_cache.get(source_key), subpath) + else: + name = ch['metadata']['name'] + raise source_exceptions.ChartSourceException(ct_type, name) + + for dep in ch.get(const.KEYWORD_DATA, {}).get('dependencies', []): + self.get_chart(dep, manifest=manifest) + + def cleanup(self): + ''' + Operations to run after deployment process has terminated + ''' + LOG.info("Performing post-flight operations.") + + # Delete temp dirs used for deployment + for chart_dir in self.chart_cache.values(): + LOG.debug('Removing temp chart directory: %s', chart_dir) + source.source_cleanup(chart_dir) diff --git a/armada/handlers/document.py b/armada/handlers/document.py index 1df25f7d..04e33d44 100644 --- a/armada/handlers/document.py +++ b/armada/handlers/document.py @@ -17,8 +17,10 @@ import urllib.parse import re -import requests from oslo_log import log as logging +import requests +import yaml +from kubernetes.client.rest import ApiException from armada.exceptions.source_exceptions import InvalidPathException from armada.utils import keystone as ks_utils @@ -30,7 +32,7 @@ class ReferenceResolver(object): """Class for handling different data references to resolve the data.""" @classmethod - def resolve_reference(cls, design_ref): + def resolve_reference(cls, design_ref, k8s=None): """Resolve a reference to a design document. Locate a schema handler based on the URI scheme of the data reference @@ -51,10 +53,16 @@ class ReferenceResolver(object): # when scheme is a empty string assume it is a local # file path - if design_uri.scheme == '': - handler = cls.scheme_handlers.get('file') - else: - handler = cls.scheme_handlers.get(design_uri.scheme, None) + scheme = design_uri.scheme or 'file' + handler = cls.scheme_handlers.get(scheme, None) + handler = handler.__get__(None, cls) + if design_uri.scheme == 'kube': + handler_2 = handler + + def handler_1(design_uri): + return handler_2(design_uri, k8s) + + handler = handler_1 if handler is None: raise InvalidPathException( @@ -71,6 +79,50 @@ class ReferenceResolver(object): return data + @classmethod + def resolve_reference_kube(cls, design_uri, k8s): + """Retrieve design documents from kubernetes crd. + + Return the result of converting the CRD to the armada/Chart/v2 schema. + + :param design_uri: Tuple as returned by urllib.parse + for the design reference + """ + if design_uri.path != '': + parts = design_uri.path.split('/') + if len(parts) != 3: + raise InvalidPathException( + "Invalid kubernetes custom resource path segment count {} " + "for '{}', expected //".format( + len(parts), design_uri.path)) + plural, namespace, name = parts + if plural != 'armadacharts': + raise InvalidPathException( + "Invalid kubernetes custom resource kind '{}' for '{}', " + "only 'armadacharts' are supported".format( + plural, design_uri.path)) + + try: + cr = k8s.read_custom_resource( + group='armada.airshipit.org', + version='v1alpha1', + namespace=namespace, + plural=plural, + name=name) + + except ApiException as err: + if err.status == 404: + raise InvalidPathException( + "Kubernetes custom resource not found: plural='{}', " + "namespace='{}', name='{}', api exception=\n{}".format( + plural, namespace, name, err.message)) + raise + + cr['schema'] = 'armada/Chart/v2' + spec = cr.pop('spec') + cr['data'] = spec + return yaml.safe_dump(cr, encoding='utf-8') + @classmethod def resolve_reference_http(cls, design_uri): """Retrieve design documents from http/https endpoints. @@ -134,6 +186,7 @@ class ReferenceResolver(object): return resp.content scheme_handlers = { + 'kube': resolve_reference_kube, 'http': resolve_reference_http, 'file': resolve_reference_file, 'https': resolve_reference_http, diff --git a/armada/handlers/manifest.py b/armada/handlers/manifest.py index 2fe42c94..221d1ebb 100644 --- a/armada/handlers/manifest.py +++ b/armada/handlers/manifest.py @@ -22,56 +22,12 @@ from armada.handlers import schema LOG = logging.getLogger(__name__) -class Manifest(object): - def __init__(self, documents, target_manifest=None): - """Instantiates a Manifest object. - - An Armada Manifest expects that at least one of each of the following - be included in ``documents``: - - * A document with schema "armada/Chart/v1" - * A document with schema "armada/ChartGroup/v1" - - And only one document of the following is allowed: - - * A document with schema "armada/Manifest/v1" - - If multiple documents with schema "armada/Manifest/v1" are provided, - specify ``target_manifest`` to select the target one. - - :param List[dict] documents: Documents out of which to build the - Armada Manifest. - :param str target_manifest: The target manifest to use when multiple - documents with "armada/Manifest/v1" are contained in - ``documents``. Default is None. - :raises ManifestException: If the expected number of document types - are not found or if the document types are missing required - properties. - """ +class Doc(object): + def __init__(self, documents): self.documents = deepcopy(documents) - self.charts, self.groups, manifests = self._find_documents( - target_manifest) + self.charts, self.groups, self.manifests = self._find_documents() - if len(manifests) > 1: - error = ( - 'Multiple manifests are not supported. Ensure that the ' - '`target_manifest` option is set to specify the target ' - 'manifest') - LOG.error(error) - raise exceptions.ManifestException(details=error) - else: - self.manifest = manifests[0] if manifests else None - - if not all([self.charts, self.groups, self.manifest]): - expected_schemas = [schema.TYPE_CHART, schema.TYPE_CHARTGROUP] - error = ( - 'Documents must include at least one of each of {} ' - 'and only one {}').format( - expected_schemas, schema.TYPE_MANIFEST) - LOG.error(error) - raise exceptions.ManifestException(details=error) - - def _find_documents(self, target_manifest=None): + def _find_documents(self): """Returns the chart documents, chart group documents, and Armada manifest @@ -97,14 +53,44 @@ class Manifest(object): if schema_info.type == schema.TYPE_CHARTGROUP: groups.append(document) if schema_info.type == schema.TYPE_MANIFEST: - manifest_name = document.get('metadata', {}).get('name') - if target_manifest: - if manifest_name == target_manifest: - manifests.append(document) - else: - manifests.append(document) + manifests.append(document) return charts, groups, manifests + def _get_target_doc(self, sch, documents, target, target_arg_name): + """Validates there is exactly one document of a given schema and + optionally name and returns it. + + :param sch: Schema which corresponds to `documents`. + :param documents: Documents which correspond to `sch`. + :param target: The target document name of schema `sch` to return. + Default is None. + :raises ManifestException: If `target` is None and multiple `documents` + are passed, or if no documents are found matching the parameters. + """ + candidates = [] + for manifest in documents: + if target: + manifest_name = manifest.get('metadata', {}).get('name') + if manifest_name == target: + candidates.append(manifest) + else: + candidates.append(manifest) + + if len(candidates) > 1: + error = ( + 'Multiple {} documents are not supported. Ensure that the ' + '`{}` option is set to specify the target one').format( + sch, target_arg_name) + LOG.error(error) + raise exceptions.ManifestException(details=error) + + if not candidates: + error = 'Documents must include at least one {}'.format(sch) + LOG.error(error) + raise exceptions.ManifestException(details=error) + + return candidates[0] + def find_chart_document(self, name): """Returns a chart document with the specified name @@ -121,22 +107,6 @@ class Manifest(object): details='Could not find {} named "{}"'.format( schema.TYPE_CHART, name)) - def find_chart_group_document(self, name): - """Returns a chart group document with the specified name - - :param str name: name of the desired chart group document - :returns: The requested chart group document - :rtype: dict - :raises ManifestException: If a chart - group document with the specified name is not found - """ - for group in self.groups: - if group.get('metadata', {}).get('name') == name: - return group - raise exceptions.BuildChartGroupException( - details='Could not find {} named "{}"'.format( - schema.TYPE_CHARTGROUP, name)) - def build_chart_deps(self, chart): """Recursively build chart dependencies for ``chart``. @@ -164,6 +134,90 @@ class Manifest(object): else: return chart + +class Chart(Doc): + def __init__(self, documents, target_chart=None): + """A Chart expects the following be included in ``documents``: + + * A document with schema "armada/Chart/v1" + + If multiple Charts are provided, specify ``target_chart`` to select the + target one. + + :param documents: Documents out of which to build the + Chart. + :param target_chart: The target Chart to use when multiple + Charts are contained in ``documents``. Default is None. + :raises ManifestException: If the expected number of document types + are not found or if the document types are missing required + properties. + """ + super(Chart, self).__init__(documents) + self.chart = self._get_target_doc( + schema.TYPE_CHART, self.documents, target_chart, 'target_chart') + + def get_chart(self): + """Builds the Chart + + :returns: The Chart. + :rtype: dict + """ + self.build_chart_deps(self.chart) + return self.chart + + +class Manifest(Doc): + def __init__(self, documents, target_manifest=None): + """An Armada Manifest expects that at least one of each of the following + be included in ``documents``: + + * A document with schema "armada/Chart/v1" + * A document with schema "armada/ChartGroup/v1" + + And only one document of the following is allowed: + + * A document with schema "armada/Manifest/v1" + + If multiple documents with schema "armada/Manifest/v1" are provided, + specify ``target_manifest`` to select the target one. + + :param List[dict] documents: Documents out of which to build the + Armada Manifest. + :param str target_manifest: The target manifest to use when multiple + documents with "armada/Manifest/v1" are contained in + ``documents``. Default is None. + :raises ManifestException: If the expected number of document types + are not found or if the document types are missing required + properties. + """ + super(Manifest, self).__init__(documents) + self.manifest = self._get_target_doc( + schema.TYPE_MANIFEST, self.manifests, target_manifest, + 'target_manifest') + + if not all([self.charts, self.groups]): + expected_schemas = [schema.TYPE_CHART, schema.TYPE_CHARTGROUP] + error = 'Documents must include at least one of each of {}'.format( + expected_schemas) + LOG.error(error) + raise exceptions.ManifestException(details=error) + + def find_chart_group_document(self, name): + """Returns a chart group document with the specified name + + :param str name: name of the desired chart group document + :returns: The requested chart group document + :rtype: dict + :raises ManifestException: If a chart + group document with the specified name is not found + """ + for group in self.groups: + if group.get('metadata', {}).get('name') == name: + return group + raise exceptions.BuildChartGroupException( + details='Could not find {} named "{}"'.format( + schema.TYPE_CHARTGROUP, name)) + def build_chart_group(self, chart_group): """Builds the chart dependencies for`charts`chart group``. diff --git a/armada/handlers/wait.py b/armada/handlers/wait.py index ee8e84da..2473047f 100644 --- a/armada/handlers/wait.py +++ b/armada/handlers/wait.py @@ -59,6 +59,9 @@ class ChartWait(): schema_info = get_schema_info(self.chart['schema']) resources = self.wait_config.get('resources') + if not resources: + resources = self.wait_config.get('resources_list') + if isinstance(resources, list): # Explicit resource config list provided. resources_list = resources diff --git a/armada/shell.py b/armada/shell.py index b6df25d3..26d032b1 100644 --- a/armada/shell.py +++ b/armada/shell.py @@ -19,6 +19,7 @@ from oslo_config import cfg from oslo_log import log from armada.cli.apply import apply_create +from armada.cli.apply_chart import apply_chart from armada.cli.delete import delete_charts from armada.cli.rollback import rollback_charts from armada.cli.test import test_charts @@ -49,6 +50,7 @@ def main(ctx, debug, api, url, token): \b $ armada apply + $ armada apply_chart $ armada delete $ armada rollback $ armada test @@ -88,6 +90,7 @@ def main(ctx, debug, api, url, token): main.add_command(apply_create) +main.add_command(apply_chart) main.add_command(delete_charts) main.add_command(rollback_charts) main.add_command(test_charts) diff --git a/armada/tests/unit/handlers/test_armada.py b/armada/tests/unit/handlers/test_armada.py index b5f2cc66..fe523227 100644 --- a/armada/tests/unit/handlers/test_armada.py +++ b/armada/tests/unit/handlers/test_armada.py @@ -143,16 +143,21 @@ data: enabled: true """ -CHART_SOURCES = [ - ('git://opendev.org/dummy/armada.git', 'chart_1'), - ('/tmp/dummy/armada', 'chart_2'), ('/tmp/dummy/armada', 'chart_3'), - ('/tmp/dummy/armada', 'chart_4') -] +CHART_SOURCES = ( + ('git://opendev.org/dummy/armada.git', + 'chart_1'), ('/tmp/dummy/armada', 'chart_2'), + ('/tmp/dummy/armada', 'chart_3'), ('/tmp/dummy/armada', 'chart_4')) # TODO(seaneagan): Add unit tests with dependencies, including transitive. class ArmadaHandlerTestCase(base.ArmadaTestCase): - def _test_pre_flight_ops(self, armada_obj): + 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 + armada_obj.pre_flight_ops() expected_config = { @@ -305,38 +310,30 @@ class ArmadaHandlerTestCase(base.ArmadaTestCase): self.assertIn('data', armada_obj.manifest) self.assertEqual(expected_config, armada_obj.manifest) - @mock.patch.object(armada, 'source') - def test_pre_flight_ops(self, mock_source): + @mock.patch.object(armada, 'ChartDownload') + def test_pre_flight_ops(self, MockChartDownload): """Test pre-flight checks and operations.""" yaml_documents = list(yaml.safe_load_all(TEST_YAML)) m_tiller = mock.Mock() m_tiller.tiller_status.return_value = True armada_obj = armada.Armada(yaml_documents, m_tiller) - # Mock methods called by `pre_flight_ops()`. - mock_source.git_clone.return_value = CHART_SOURCES[0][0] + self._test_pre_flight_ops(armada_obj, MockChartDownload) - self._test_pre_flight_ops(armada_obj) + MockChartDownload.return_value.get_chart.assert_called() - mock_source.git_clone.assert_called_once_with( - 'git://opendev.org/dummy/armada.git', - 'master', - auth_method=None, - proxy_server=None) - - @mock.patch.object(armada, 'source') - def test_post_flight_ops(self, mock_source): + @mock.patch.object(armada, 'ChartDownload') + def test_post_flight_ops(self, MockChartDownload): """Test post-flight operations.""" yaml_documents = list(yaml.safe_load_all(TEST_YAML)) # Mock methods called by `pre_flight_ops()`. m_tiller = mock.Mock() m_tiller.tiller_status.return_value = True - mock_source.git_clone.return_value = CHART_SOURCES[0][0] armada_obj = armada.Armada(yaml_documents, m_tiller) - self._test_pre_flight_ops(armada_obj) + self._test_pre_flight_ops(armada_obj, MockChartDownload) armada_obj.post_flight_ops() @@ -345,8 +342,7 @@ class ArmadaHandlerTestCase(base.ArmadaTestCase): const.KEYWORD_CHARTS)): if chart.get( const.KEYWORD_DATA).get('source').get('type') == 'git': - mock_source.source_cleanup.assert_called_with( - CHART_SOURCES[counter][0]) + MockChartDownload.return_value.cleanup.assert_called_with() # TODO(seaneagan): Separate ChartDeploy tests into separate module. # TODO(seaneagan): Once able to make mock library sufficiently thread safe, @@ -671,19 +667,17 @@ class ArmadaHandlerTestCase(base.ArmadaTestCase): class ArmadaNegativeHandlerTestCase(base.ArmadaTestCase): - @mock.patch.object(armada, 'source') - def test_armada_get_manifest_exception(self, mock_source): + @mock.patch.object(armada, 'ChartDownload') + def test_armada_get_manifest_exception(self, MockChartDownload): """Test armada handling with invalid manifest.""" yaml_documents = list(yaml.safe_load_all(TEST_YAML)) - error_re = ( - '.*Documents must include at least one of each of .* and ' - 'only one .*') + error_re = ('.*Documents must include at least one of each of .*') self.assertRaisesRegexp( ManifestException, error_re, armada.Armada, yaml_documents[:1], mock.MagicMock()) - @mock.patch.object(armada, 'source') - def test_armada_override_exception(self, mock_source): + @mock.patch.object(armada, 'ChartDownload') + def test_armada_override_exception(self, MockChartDownload): """Test Armada checks with invalid chart override.""" yaml_documents = list(yaml.safe_load_all(TEST_YAML)) override = ('chart:example-chart-2:name=' 'overridden', ) @@ -692,8 +686,8 @@ class ArmadaNegativeHandlerTestCase(base.ArmadaTestCase): with self.assertRaisesRegexp(InvalidOverrideValueException, error_re): armada.Armada(yaml_documents, mock.MagicMock(), set_ovr=override) - @mock.patch.object(armada, 'source') - def test_armada_manifest_exception_override_none(self, mock_source): + @mock.patch.object(armada, 'ChartDownload') + def test_armada_manifest_exception_override_none(self, MockChartDownload): """Test Armada checks with invalid manifest.""" yaml_documents = list(yaml.safe_load_all(TEST_YAML)) example_document = [ diff --git a/armada/tests/unit/handlers/test_manifest.py b/armada/tests/unit/handlers/test_manifest.py index f76c37dd..17057d4d 100644 --- a/armada/tests/unit/handlers/test_manifest.py +++ b/armada/tests/unit/handlers/test_manifest.py @@ -15,7 +15,7 @@ import copy import os -import testtools +import testtools import yaml from armada import exceptions @@ -117,7 +117,7 @@ class ManifestTestCase(testtools.TestCase): def test_find_documents(self): armada_manifest = manifest.Manifest(self.documents) chart_documents, chart_groups, manifests = armada_manifest. \ - _find_documents(target_manifest='armada-manifest') + _find_documents() # checking if all the chart documents are present self.assertIsInstance(chart_documents, list) @@ -344,7 +344,8 @@ class ManifestNegativeTestCase(testtools.TestCase): documents = copy.deepcopy(self.documents) documents.append(documents[-1]) # Copy the last manifest. - error_re = r'Multiple manifests are not supported.*' + error_re = r'Multiple {} documents are not supported.*'.format( + schema.TYPE_MANIFEST) self.assertRaisesRegexp( exceptions.ManifestException, error_re, manifest.Manifest, documents) @@ -355,7 +356,8 @@ class ManifestNegativeTestCase(testtools.TestCase): documents = copy.deepcopy(self.documents) documents.append(documents[-1]) # Copy the last manifest. - error_re = r'Multiple manifests are not supported.*' + error_re = r'Multiple {} documents are not supported.*'.format( + schema.TYPE_MANIFEST) self.assertRaisesRegexp( exceptions.ManifestException, error_re, @@ -364,9 +366,7 @@ class ManifestNegativeTestCase(testtools.TestCase): target_manifest='armada-manifest') def _assert_missing_documents_raises(self, documents): - error_re = ( - '.*Documents must include at least one of each of .* and ' - 'only one .*') + error_re = ('.*Documents must include at least one of each of .*') self.assertRaisesRegexp( exceptions.ManifestException, error_re, manifest.Manifest, documents) @@ -374,7 +374,11 @@ class ManifestNegativeTestCase(testtools.TestCase): def test_get_documents_missing_manifest(self): # Validates exceptions.ManifestException is thrown if no manifest is # found. Manifest is last document in sample YAML. - self._assert_missing_documents_raises(self.documents[:-1]) + error_re = 'Documents must include at least one {}'.format( + schema.TYPE_MANIFEST) + self.assertRaisesRegexp( + exceptions.ManifestException, error_re, manifest.Manifest, + self.documents[:-1]) def test_get_documents_missing_charts(self): # Validates exceptions.ManifestException is thrown if no chart is diff --git a/doc/source/commands/apply_chart.rst b/doc/source/commands/apply_chart.rst new file mode 100644 index 00000000..bdf725cf --- /dev/null +++ b/doc/source/commands/apply_chart.rst @@ -0,0 +1,53 @@ +Armada - Apply Chart +==================== + + +Commands +-------- + +.. code:: bash + + Usage: armada apply_chart [OPTIONS] [LOCATION] + + This command installs and updates an Armada chart. + + [LOCATION] must be a relative path to Armada Chart or a reference + to an Armada Chart kubernetes CR which has the same format, except as + noted in the :ref:`v2 document authoring documentation `. + + To install or upgrade a chart, run: + + $ armada apply_chart --release-prefix=armada my-chart.yaml + $ armada apply_chart --release-prefix=armada kube:armadacharts/my-namespace/my-chart + + Options: + --release-prefix TEXT Release prefix to use. [required] + --disable-update-post Disable post-update Tiller operations. + --disable-update-pre Disable pre-update Tiller operations. + --metrics-output TEXT Output path for prometheus metric data, should + end in .prom. By default, no metric data is + output. + --tiller-host TEXT Tiller host IP. + --tiller-port INTEGER Tiller host port. + -tn, --tiller-namespace TEXT Tiller namespace. + --timeout INTEGER Specifies time to wait for each chart to fully + finish deploying. + --wait Force Tiller to wait until the chart is + deployed, rather than using the charts + specified wait policy. This is equivalent to + sequenced chartgroups. + --target-chart TEXT The target chart to deploy. Required for + specifying which chart to deploy when multiple + are available. + --bearer-token TEXT User Bearer token + --debug Enable debug logging. + --help Show this message and exit. + +Synopsis +-------- + +The apply_chart command will deploy an armada chart definition, installing or +updating as appropriate. + +``armada apply_chart --release-prefix=armada my-chart.yaml [--debug]`` +``armada apply_chart --release-prefix=armada kube:armadacharts/my-namespace/my-chart [--debug]`` diff --git a/doc/source/commands/index.rst b/doc/source/commands/index.rst index c76270a0..4857541e 100644 --- a/doc/source/commands/index.rst +++ b/doc/source/commands/index.rst @@ -11,6 +11,7 @@ Commands Guide :caption: Contents: apply.rst + apply_chart.rst rollback.rst test.rst tiller.rst diff --git a/doc/source/operations/documents/v2/document-authoring.rst b/doc/source/operations/documents/v2/document-authoring.rst index 52b19b40..dfc1e94c 100644 --- a/doc/source/operations/documents/v2/document-authoring.rst +++ b/doc/source/operations/documents/v2/document-authoring.rst @@ -124,6 +124,7 @@ Chart | dependencies | object | (optional) Override the `builtin chart dependencies`_ with a list of Chart documents | | | | to use as dependencies instead. | | | | NOTE: Builtin ".tgz" dependencies are not yet supported. | +| | | NOTE: This field is not supported in the ArmadaChart CRD. | +-----------------+----------+---------------------------------------------------------------------------------------+ .. _wait_v2: @@ -154,6 +155,8 @@ Wait | | | **array** - Lists all `Wait Resource`_ s to use, completely | | | | overriding the default. Can be set to ``[]`` to disable all | | | | resource types. | +| | | NOTE: To use the array form with the ArmadaChart CRD, the keyword | +| | | must be ``resources_list`` instead of ``resources``. | | | | | | | | See also `Wait Resources Examples`_. | +-------------+----------+--------------------------------------------------------------------+ diff --git a/manifests/chart-crd.yaml b/manifests/chart-crd.yaml new file mode 100644 index 00000000..ef604e53 --- /dev/null +++ b/manifests/chart-crd.yaml @@ -0,0 +1,165 @@ + +apiVersion: "apiextensions.k8s.io/v1" +kind: "CustomResourceDefinition" +metadata: + name: "armadacharts.armada.airshipit.org" +spec: + group: "armada.airshipit.org" + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + release: + type: string + namespace: + type: string + values: + type: object + x-kubernetes-preserve-unknown-fields: true + protected: + type: object + properties: + continue_processing: + type: boolean + test: + type: object + properties: + enabled: + type: boolean + timeout: + type: integer + options: + type: object + properties: + cleanup: + type: boolean + wait: + type: object + properties: + timeout: + type: integer + resources: + type: object + additionalProperties: + type: array + items: + type: object + properties: + labels: + type: object + additionalProperties: + type: string + min_ready: + x-kubernetes-int-or-string: true + anyOf: + - type: integer + - type: string + required: + type: boolean + resources_list: + type: array + items: + type: object + properties: + labels: + type: object + additionalProperties: + type: string + min_ready: + x-kubernetes-int-or-string: true + anyOf: + - type: integer + - type: string + required: + type: boolean + type: + type: string + required: + - type + labels: + type: object + additionalProperties: + type: string + native: + type: object + properties: + enabled: + type: boolean + # Note: This is specific to the kubernetes schema. + # Dynamically typed fields are disallowed by kubernetes + # structural schemas, so object and list resource overrides + # need two separate fields. We specify here that, exactly one + # of these can be given. + not: + allOf: + - required: + - resources + - required: + - resources_list + source: + type: object + properties: + type: + type: string + location: + type: string + subpath: + type: string + reference: + type: string + proxy_server: + type: string + auth_method: + type: string + required: + - location + - type + delete: + type: object + properties: + timeout: + type: integer + upgrade: + type: object + properties: + pre: + type: object + properties: + delete: + type: array + items: + type: object + properties: + type: + type: string + labels: + type: object + additionalProperties: + type: string + required: + - type + options: + type: object + properties: + force: + type: boolean + recreate_pods: + type: boolean + no_hooks: + type: boolean + required: + - namespace + - release + - source + scope: "Namespaced" + names: + plural: "armadacharts" + singular: "armadachart" + kind: "ArmadaChart" diff --git a/manifests/kustomization.yaml b/manifests/kustomization.yaml new file mode 100644 index 00000000..067128b7 --- /dev/null +++ b/manifests/kustomization.yaml @@ -0,0 +1,3 @@ +resources: + - chart-crd.yaml + - rbac.yaml diff --git a/manifests/rbac.yaml b/manifests/rbac.yaml new file mode 100644 index 00000000..78c19069 --- /dev/null +++ b/manifests/rbac.yaml @@ -0,0 +1,62 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: armada-controller + labels: + rbac.armada.airshipit.org/aggregate-to-armada: "true" +rules: + - apiGroups: + - "apps" + resources: + - deployments + - statefulsets + - daemonsets + verbs: + - get + - list + - watch + - apiGroups: + - batch + - extensions + resources: + - jobs + verbs: + - get + - list + - watch + - delete + - apiGroups: + - "" + resources: + - pods + verbs: + - get + - list + - watch + - delete + - apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - get + - create + - apiGroups: + - armada.process + resources: + - locks + verbs: + - get + - list + - create + - delete + - patch + - update + - apiGroups: + - armada.airshipit.org + resources: + - armadacharts + verbs: + - get + - list +--- diff --git a/tools/airship2-integration/000-clone-dependencies.sh b/tools/airship2-integration/000-clone-dependencies.sh new file mode 100755 index 00000000..f98306b6 --- /dev/null +++ b/tools/airship2-integration/000-clone-dependencies.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# Copyright 2020 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. + +set -xe + +CURRENT_DIR="$(pwd)" +: "${INSTALL_PATH:="../"}" +cd ${INSTALL_PATH} +: "${OSH_INFRA_COMMIT:="eacf93722136636dcfbd2b68c59b71f071ffc085"}" + +# Clone openstack-helm-infra +git clone https://opendev.org/openstack/openstack-helm-infra.git +cd openstack-helm-infra +git checkout "${OSH_INFRA_COMMIT}" + +cd "${CURRENT_DIR}" diff --git a/tools/airship2-integration/010-deploy-k8s.sh b/tools/airship2-integration/010-deploy-k8s.sh new file mode 100755 index 00000000..e0c2ddf4 --- /dev/null +++ b/tools/airship2-integration/010-deploy-k8s.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +# Copyright 2019, AT&T Intellectual Property +# +# 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. + +set -xe + +CURRENT_DIR="$(pwd)" +: "${OSH_INFRA_PATH:="../openstack-helm-infra"}" + +# Configure proxy settings if $PROXY is set +if [ -n "${PROXY}" ]; then + . tools/deployment/airskiff/common/setup-proxy.sh +fi + +# Deploy K8s with Minikube +cd "${OSH_INFRA_PATH}" +bash -c "./tools/deployment/common/005-deploy-k8s.sh" + +# Add user to Docker group +# NOTE: This requires re-authentication. Restart your shell. +sudo adduser "$(whoami)" docker +sudo su - "$USER" -c bash <<'END_SCRIPT' +if echo $(groups) | grep -qv 'docker'; then + echo "You need to logout to apply group permissions" + echo "Please logout and login" +fi +END_SCRIPT + +# clean up /etc/resolv.conf, if it includes a localhost dns address +sudo sed -i.bkp '/^nameserver.*127.0.0.1/d + w /dev/stdout' /etc/resolv.conf + +cd "${CURRENT_DIR}" diff --git a/tools/airship2-integration/020-apply-charts.sh b/tools/airship2-integration/020-apply-charts.sh new file mode 100755 index 00000000..69aca674 --- /dev/null +++ b/tools/airship2-integration/020-apply-charts.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# Copyright 2020 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. + +set -xe + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +./tools/airship2-integration/test/test.sh basic diff --git a/tools/airship2-integration/test/examples/basic/a1.yaml b/tools/airship2-integration/test/examples/basic/a1.yaml new file mode 100644 index 00000000..430b02da --- /dev/null +++ b/tools/airship2-integration/test/examples/basic/a1.yaml @@ -0,0 +1,24 @@ +apiVersion: "armada.airshipit.org/v1alpha1" +kind: "ArmadaChart" +metadata: + name: a1 + namespace: test +spec: + release: a1 + namespace: test + wait: + timeout: 100 + labels: + release_group: armada-a1 + source: + location: https://kubernetes-charts-incubator.storage.googleapis.com/raw-0.2.3.tgz + subpath: raw + type: tar + values: + resources: + - apiVersion: v1 + kind: ConfigMap + metadata: + name: a1 + data: + chart: a1 diff --git a/tools/airship2-integration/test/examples/basic/a2.yaml b/tools/airship2-integration/test/examples/basic/a2.yaml new file mode 100644 index 00000000..2ae460c4 --- /dev/null +++ b/tools/airship2-integration/test/examples/basic/a2.yaml @@ -0,0 +1,24 @@ +apiVersion: "armada.airshipit.org/v1alpha1" +kind: "ArmadaChart" +metadata: + name: a2 + namespace: test +spec: + release: a2 + namespace: test + wait: + timeout: 100 + labels: + release_group: armada-a1 + source: + location: https://kubernetes-charts-incubator.storage.googleapis.com/raw-0.2.3.tgz + subpath: raw + type: tar + values: + resources: + - apiVersion: v1 + kind: ConfigMap + metadata: + name: a2 + data: + chart: a2 diff --git a/tools/airship2-integration/test/examples/basic/b1.yaml b/tools/airship2-integration/test/examples/basic/b1.yaml new file mode 100644 index 00000000..b02b44a1 --- /dev/null +++ b/tools/airship2-integration/test/examples/basic/b1.yaml @@ -0,0 +1,24 @@ +apiVersion: "armada.airshipit.org/v1alpha1" +kind: "ArmadaChart" +metadata: + name: b1 + namespace: test +spec: + release: b1 + namespace: test + wait: + timeout: 100 + labels: + release_group: armada-b1 + source: + location: https://kubernetes-charts-incubator.storage.googleapis.com/raw-0.2.3.tgz + subpath: raw + type: tar + values: + resources: + - apiVersion: v1 + kind: ConfigMap + metadata: + name: b1 + data: + chart: b1 diff --git a/tools/airship2-integration/test/examples/basic/b2.yaml b/tools/airship2-integration/test/examples/basic/b2.yaml new file mode 100644 index 00000000..101fba24 --- /dev/null +++ b/tools/airship2-integration/test/examples/basic/b2.yaml @@ -0,0 +1,24 @@ +apiVersion: "armada.airshipit.org/v1alpha1" +kind: "ArmadaChart" +metadata: + name: b2 + namespace: test +spec: + release: b2 + namespace: test + wait: + timeout: 100 + labels: + release_group: armada-b2 + source: + location: https://kubernetes-charts-incubator.storage.googleapis.com/raw-0.2.3.tgz + subpath: raw + type: tar + values: + resources: + - apiVersion: v1 + kind: ConfigMap + metadata: + name: b2 + data: + chart: b2 diff --git a/tools/airship2-integration/test/examples/basic/b3.yaml b/tools/airship2-integration/test/examples/basic/b3.yaml new file mode 100644 index 00000000..2d08eb98 --- /dev/null +++ b/tools/airship2-integration/test/examples/basic/b3.yaml @@ -0,0 +1,24 @@ +apiVersion: "armada.airshipit.org/v1alpha1" +kind: "ArmadaChart" +metadata: + name: b3 + namespace: test +spec: + release: b3 + namespace: test + wait: + timeout: 100 + labels: + release_group: armada-b3 + source: + location: https://kubernetes-charts-incubator.storage.googleapis.com/raw-0.2.3.tgz + subpath: raw + type: tar + values: + resources: + - apiVersion: v1 + kind: ConfigMap + metadata: + name: b3 + data: + chart: b3 diff --git a/tools/airship2-integration/test/examples/basic/c1.yaml b/tools/airship2-integration/test/examples/basic/c1.yaml new file mode 100644 index 00000000..e242e25f --- /dev/null +++ b/tools/airship2-integration/test/examples/basic/c1.yaml @@ -0,0 +1,24 @@ +apiVersion: "armada.airshipit.org/v1alpha1" +kind: "ArmadaChart" +metadata: + name: c1 + namespace: test +spec: + release: c1 + namespace: test + wait: + timeout: 100 + labels: + release_group: armada-c1 + source: + location: https://kubernetes-charts-incubator.storage.googleapis.com/raw-0.2.3.tgz + subpath: raw + type: tar + values: + resources: + - apiVersion: v1 + kind: ConfigMap + metadata: + name: c1 + data: + chart: c1 diff --git a/tools/airship2-integration/test/test-job.yaml b/tools/airship2-integration/test/test-job.yaml new file mode 100644 index 00000000..ff67aba3 --- /dev/null +++ b/tools/airship2-integration/test/test-job.yaml @@ -0,0 +1,51 @@ +# Copyright (c) 2020 AT&T Intellectual Property. All 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. +--- +apiVersion: batch/v1 +kind: Job +metadata: + namespace: "${NAMESPACE}" + name: "${JOB_NAME}" +spec: + backoffLimit: 0 + template: + spec: + serviceAccountName: ${SERVICE_ACCOUNT} + restartPolicy: Never + containers: + - name: "test-airship2-integration" + image: "${IMAGE}" + imagePullPolicy: Never + volumeMounts: + - name: kube-config + mountPath: /armada/.kube/config + command: + - /bin/bash + - -c + - |- + set -xe + + apply_chart() { + NAME=$1 + armada apply_chart kube:armadacharts/$NAMESPACE/${DOLLAR}NAME --release-prefix ${RELEASE_PREFIX} + } + + for CHART in ${CHARTS_SPACE_SEPARATED}; do + apply_chart ${DOLLAR}CHART + done + volumes: + - name: kube-config + hostPath: + path: "${KUBE_CONFIG}" +... diff --git a/tools/airship2-integration/test/test-namespace.yaml b/tools/airship2-integration/test/test-namespace.yaml new file mode 100644 index 00000000..b8e69c7c --- /dev/null +++ b/tools/airship2-integration/test/test-namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: ${NAMESPACE} diff --git a/tools/airship2-integration/test/test-rbac.yaml b/tools/airship2-integration/test/test-rbac.yaml new file mode 100644 index 00000000..591c914b --- /dev/null +++ b/tools/airship2-integration/test/test-rbac.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: ${SERVICE_ACCOUNT} + namespace: ${NAMESPACE} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: ${SERVICE_ACCOUNT}-armada-controller +subjects: + - kind: ServiceAccount + name: ${SERVICE_ACCOUNT} + namespace: ${NAMESPACE} +roleRef: + kind: ClusterRole + name: armada-controller + apiGroup: rbac.authorization.k8s.io diff --git a/tools/airship2-integration/test/test.sh b/tools/airship2-integration/test/test.sh new file mode 100755 index 00000000..1de3455d --- /dev/null +++ b/tools/airship2-integration/test/test.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +# Copyright 2020 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. + +set -xe + +EXAMPLE=$1 + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +export NAMESPACE=test +export SERVICE_ACCOUNT=test-armada +export KUBE_CONFIG=~/.kube/config +export RELEASE_PREFIX=test +export JOB_NAME=apply-chart-test +export IMAGE=quay.io/airshipit/armada:latest-ubuntu_bionic +TIMEOUT=300 +# See https://stackoverflow.com/a/24964089 +export DOLLAR="\$" + +# Cleanup any previous runs +cleanup() { + kubectl delete namespace $NAMESPACE --ignore-not-found=true + for i in $(helm ls --short | grep $RELEASE_PREFIX-); do helm del --purge $i; done +} +cleanup + +# Install namespace +envsubst < $DIR/test-namespace.yaml | kubectl apply -f - +# Install CRD +kubectl apply -k ./manifests +# Install RBAC +envsubst < $DIR/test-rbac.yaml | kubectl apply -f - +# Install example CRs +kubectl apply -R -f $DIR/examples/$EXAMPLE + +# Run test +export CHARTS=$(kubectl get armadacharts -n $NAMESPACE -o name | cut -d / -f2) +export CHARTS_SPACE_SEPARATED=$(echo "$CHARTS" | tr "\n" " ") + +envsubst < $DIR/test-job.yaml | kubectl create -f - +# Wait for test job completion +kubectl wait --timeout ${TIMEOUT}s --for=condition=Complete -n $NAMESPACE job/$JOB_NAME +POD_NAME=$(kubectl get pods -n $NAMESPACE -l job-name=$JOB_NAME -o json | jq -r '.items[0].metadata.name') +kubectl logs -n $NAMESPACE $POD_NAME + +ACTUAL=$(helm ls --short) +EXPECTED=$(echo "$CHARTS" | sed -e "s/^/$RELEASE_PREFIX-/") +diff <(echo "$ACTUAL") <(echo "$EXPECTED") diff --git a/tools/gate/playbooks/airship2-integration.yaml b/tools/gate/playbooks/airship2-integration.yaml new file mode 100644 index 00000000..b86a8387 --- /dev/null +++ b/tools/gate/playbooks/airship2-integration.yaml @@ -0,0 +1,45 @@ +# Copyright 2018 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. + +- hosts: primary + tasks: + - name: Clone Required Repositories + shell: | + ./tools/airship2-integration/000-clone-dependencies.sh + args: + chdir: "{{ zuul.project.src_dir }}" + + - name: Deploy Kubernetes with Minikube + shell: | + ./tools/airship2-integration/010-deploy-k8s.sh + args: + chdir: "{{ zuul.project.src_dir }}" + + - name: Build Armada with submitted changes + shell: | + # Add image to minikube + eval $(minikube docker-env) + make images + args: + chdir: "{{ zuul.project.src_dir }}" + become: yes + + - name: Apply charts + shell: | + mkdir ~/.kube + cp -rp /home/zuul/.kube/config ~/.kube/config + ./tools/airship2-integration/020-apply-charts.sh + args: + chdir: "{{ zuul.project.src_dir }}" + become: yes