From c75898cd6aca7299481e0ed99c262770308af155 Mon Sep 17 00:00:00 2001 From: Sean Eagan Date: Fri, 6 Dec 2019 16:02:03 -0600 Subject: [PATCH] Airship 2 support features Airship 2 is using Argo for workflow management, rather than the builtin Armada workflow functionality. Hence, this adds an apply_chart CLI command to apply a single chart at a time, so that Argo can manage the higher level orchestration. Airship 2 is also using kubernetes as opposed to Deckhand as the document store. Hence this adds an ArmadaChart kubernetes CRD, which can be consumed by the apply_chart CLI command. The chart `dependencies` feature is intentionally not supported by the CRD, as there are additional complexities to make that work, and ideally this feature should be deprecated as charts should be building in there dependencies before consumption by Armada. Functional tests are included to excercise these features against a minikube cluster. Change-Id: I2bbed83d6d80091322a7e60b918a534188467239 --- .zuul.yaml | 21 ++ armada/cli/apply_chart.py | 207 ++++++++++++++++++ armada/handlers/armada.py | 76 +------ armada/handlers/chart_deploy.py | 6 +- armada/handlers/chart_download.py | 104 +++++++++ armada/handlers/document.py | 65 +++++- armada/handlers/manifest.py | 194 ++++++++++------ armada/handlers/wait.py | 3 + armada/shell.py | 3 + armada/tests/unit/handlers/test_armada.py | 58 +++-- armada/tests/unit/handlers/test_manifest.py | 20 +- doc/source/commands/apply_chart.rst | 53 +++++ doc/source/commands/index.rst | 1 + .../documents/v2/document-authoring.rst | 3 + manifests/chart-crd.yaml | 165 ++++++++++++++ manifests/kustomization.yaml | 3 + manifests/rbac.yaml | 62 ++++++ .../000-clone-dependencies.sh | 28 +++ tools/airship2-integration/010-deploy-k8s.sh | 45 ++++ .../airship2-integration/020-apply-charts.sh | 20 ++ .../test/examples/basic/a1.yaml | 24 ++ .../test/examples/basic/a2.yaml | 24 ++ .../test/examples/basic/b1.yaml | 24 ++ .../test/examples/basic/b2.yaml | 24 ++ .../test/examples/basic/b3.yaml | 24 ++ .../test/examples/basic/c1.yaml | 24 ++ tools/airship2-integration/test/test-job.yaml | 51 +++++ .../test/test-namespace.yaml | 4 + .../airship2-integration/test/test-rbac.yaml | 18 ++ tools/airship2-integration/test/test.sh | 61 ++++++ .../gate/playbooks/airship2-integration.yaml | 45 ++++ 31 files changed, 1270 insertions(+), 190 deletions(-) create mode 100644 armada/cli/apply_chart.py create mode 100644 armada/handlers/chart_download.py create mode 100644 doc/source/commands/apply_chart.rst create mode 100644 manifests/chart-crd.yaml create mode 100644 manifests/kustomization.yaml create mode 100644 manifests/rbac.yaml create mode 100755 tools/airship2-integration/000-clone-dependencies.sh create mode 100755 tools/airship2-integration/010-deploy-k8s.sh create mode 100755 tools/airship2-integration/020-apply-charts.sh create mode 100644 tools/airship2-integration/test/examples/basic/a1.yaml create mode 100644 tools/airship2-integration/test/examples/basic/a2.yaml create mode 100644 tools/airship2-integration/test/examples/basic/b1.yaml create mode 100644 tools/airship2-integration/test/examples/basic/b2.yaml create mode 100644 tools/airship2-integration/test/examples/basic/b3.yaml create mode 100644 tools/airship2-integration/test/examples/basic/c1.yaml create mode 100644 tools/airship2-integration/test/test-job.yaml create mode 100644 tools/airship2-integration/test/test-namespace.yaml create mode 100644 tools/airship2-integration/test/test-rbac.yaml create mode 100755 tools/airship2-integration/test/test.sh create mode 100644 tools/gate/playbooks/airship2-integration.yaml 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