diff --git a/.dockerignore b/.dockerignore index 18c23416..08cd7984 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,3 +5,6 @@ CODE_OF_CONDUCT.rst ChangeLog LICENSE OWNERS +etc/armada/armada.conf +etc/armada/policy.yaml +charts/* diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index e51474d0..00000000 --- a/.editorconfig +++ /dev/null @@ -1,18 +0,0 @@ -# EditorConfig http://editorconfig.org - -root = true - -[*] -indent_style = space -indent_size = 4 -tab_width = 4 -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = false -insert_final_newline = true -max_line_length = 80 -curly_bracket_next_line = false -spaces_around_operators = true -spaces_around_brackets = true -indent_brace_style = K&R - diff --git a/.gitignore b/.gitignore index a91da7da..8a300dde 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,9 @@ var/ .installed.cfg *.egg etc/*.sample +etc/hostname +etc/hosts +etc/resolv.conf # PyInstaller # Usually these files are written by a python script from a template @@ -97,3 +100,9 @@ ENV/ **/*.tgz **/_partials.tpl **/_globals.tpl + +AUTHORS +ChangeLog +etc/armada/armada.conf +etc/armada/policy.yaml +.editorconfig diff --git a/Dockerfile b/Dockerfile index 6baeaa7a..a037824a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,8 @@ FROM ubuntu:16.04 MAINTAINER Armada Team ENV DEBIAN_FRONTEND noninteractive +ENV LANG=C.UTF-8 +ENV LC_ALL=C.UTF-8 COPY . /armada diff --git a/armada/api/__init__.py b/armada/api/__init__.py index cf92688e..f055aeb1 100644 --- a/armada/api/__init__.py +++ b/armada/api/__init__.py @@ -10,22 +10,32 @@ # 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. +# limitations under the License import json -import uuid import logging as log +import os +import uuid +import yaml import falcon +from oslo_config import cfg from oslo_log import log as logging -LOG = logging.getLogger(__name__) +from armada import const + +CONF = cfg.CONF class BaseResource(object): def __init__(self): - self.logger = LOG + if not (os.path.exists(const.CONFIG_PATH)): + logging.register_options(CONF) + logging.set_defaults(default_log_levels=CONF.default_log_levels) + logging.setup(CONF, 'armada') + + self.logger = logging.getLogger(__name__) def on_options(self, req, resp): self_attrs = dir(self) @@ -39,29 +49,23 @@ class BaseResource(object): resp.headers['Allow'] = ','.join(allowed_methods) resp.status = falcon.HTTP_200 - def req_json(self, req): + def req_yaml(self, req): if req.content_length is None or req.content_length == 0: return None - if req.content_type is not None and req.content_type.lower( - ) == 'application/json': - raw_body = req.stream.read(req.content_length or 0) + raw_body = req.stream.read(req.content_length or 0) - if raw_body is None: - return None + if raw_body is None: + return None - try: - # json_body = json.loads(raw_body.decode('utf-8')) - # return json_body - return raw_body - except json.JSONDecodeError as jex: - self.error( - req.context, - "Invalid JSON in request: \n%s" % raw_body.decode('utf-8')) - raise json.JSONDecodeError("%s: Invalid JSON in body: %s" % - (req.path, jex)) - else: - raise json.JSONDecodeError("Requires application/json payload") + try: + return yaml.safe_load_all(raw_body.decode('utf-8')) + except yaml.YAMLError as jex: + self.error( + req.context, + "Invalid YAML in request: \n%s" % raw_body.decode('utf-8')) + raise Exception( + "%s: Invalid YAML in body: %s" % (req.path, jex)) def return_error(self, resp, status_code, message="", retry=False): resp.body = json.dumps({ diff --git a/armada/api/armada_controller.py b/armada/api/armada_controller.py deleted file mode 100644 index 62ad12de..00000000 --- a/armada/api/armada_controller.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright 2017 The Armada Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import json - -import falcon -from oslo_log import log as logging - -from armada import api -from armada.handlers.armada import Armada - -LOG = logging.getLogger(__name__) - - -class Apply(api.BaseResource): - ''' - apply armada endpoint service - ''' - - def on_post(self, req, resp): - try: - - # Load data from request and get options - data = self.req_json(req) - opts = {} - # opts = data['options'] - - # Encode filename - # data['file'] = data['file'].encode('utf-8') - armada = Armada( - data, - disable_update_pre=opts.get('disable_update_pre', False), - disable_update_post=opts.get('disable_update_post', False), - enable_chart_cleanup=opts.get('enable_chart_cleanup', False), - dry_run=opts.get('dry_run', False), - wait=opts.get('wait', False), - timeout=opts.get('timeout', False)) - - msg = armada.sync() - - resp.data = json.dumps({'message': msg}) - - resp.content_type = 'application/json' - resp.status = falcon.HTTP_200 - except Exception as e: - self.error(req.context, "Failed to apply manifest") - self.return_error( - resp, falcon.HTTP_500, - message="Failed to install manifest: {} {}".format(e, data)) diff --git a/armada/api/controller/armada.py b/armada/api/controller/armada.py new file mode 100644 index 00000000..ad298db4 --- /dev/null +++ b/armada/api/controller/armada.py @@ -0,0 +1,71 @@ +# Copyright 2017 The Armada Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json + +import falcon + +from armada import api +from armada.common import policy +from armada.handlers.armada import Armada + + +class Apply(api.BaseResource): + ''' + apply armada endpoint service + ''' + @policy.enforce('armada:create_endpoints') + def on_post(self, req, resp): + try: + + # Load data from request and get options + + data = list(self.req_yaml(req)) + + if type(data[0]) is list: + data = list(data[0]) + + opts = req.params + + # Encode filename + armada = Armada( + data, + disable_update_pre=req.get_param_as_bool( + 'disable_update_pre'), + disable_update_post=req.get_param_as_bool( + 'disable_update_post'), + enable_chart_cleanup=req.get_param_as_bool( + 'enable_chart_cleanup'), + dry_run=req.get_param_as_bool('dry_run'), + wait=req.get_param_as_bool('wait'), + timeout=int(opts.get('timeout', 3600)), + tiller_host=opts.get('tiller_host', None), + tiller_port=int(opts.get('tiller_port', 44134)), + ) + + msg = armada.sync() + + resp.body = json.dumps( + { + 'message': msg, + } + ) + + resp.content_type = 'application/json' + resp.status = falcon.HTTP_200 + except Exception as e: + err_message = 'Failed to apply manifest: {}'.format(e) + self.error(req.context, err_message) + self.return_error( + resp, falcon.HTTP_500, message=err_message) diff --git a/armada/api/controller/test.py b/armada/api/controller/test.py new file mode 100644 index 00000000..3a31fd7c --- /dev/null +++ b/armada/api/controller/test.py @@ -0,0 +1,134 @@ +# Copyright 2017 The Armada Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json + +import falcon + +from armada import api +from armada.common import policy +from armada import const +from armada.handlers.tiller import Tiller +from armada.handlers.manifest import Manifest +from armada.utils.release import release_prefix + + +class Test(api.BaseResource): + ''' + Test helm releases via release name + ''' + + @policy.enforce('armada:test_release') + def on_get(self, req, resp, release): + try: + self.logger.info('RUNNING: %s', release) + opts = req.params + tiller = Tiller(tiller_host=opts.get('tiller_host', None), + tiller_port=opts.get('tiller_port', None)) + tiller_resp = tiller.testing_release(release) + msg = { + 'result': '', + 'message': '' + } + + if tiller_resp: + test_status = getattr( + tiller_resp.info.status, 'last_test_suite_run', 'FAILED') + + if test_status.result[0].status: + msg['result'] = 'PASSED: {}'.format(release) + msg['message'] = 'MESSAGE: Test Pass' + self.logger.info(msg) + else: + msg['result'] = 'FAILED: {}'.format(release) + msg['message'] = 'MESSAGE: Test Fail' + self.logger.info(msg) + else: + msg['result'] = 'FAILED: {}'.format(release) + msg['message'] = 'MESSAGE: No test found' + + resp.body = json.dumps(msg) + resp.status = falcon.HTTP_200 + resp.content_type = 'application/json' + + except Exception as e: + err_message = 'Failed to test {}: {}'.format(release, e) + self.error(req.context, err_message) + self.return_error( + resp, falcon.HTTP_500, message=err_message) + + +class Tests(api.BaseResource): + ''' + Test helm releases via a manifest + ''' + + @policy.enforce('armada:tests_manifest') + def on_post(self, req, resp): + try: + opts = req.params + tiller = Tiller(tiller_host=opts.get('tiller_host', None), + tiller_port=opts.get('tiller_port', None)) + + documents = self.req_yaml(req) + armada_obj = Manifest(documents).get_manifest() + prefix = armada_obj.get(const.KEYWORD_ARMADA).get( + const.KEYWORD_PREFIX) + known_releases = [release[0] for release in tiller.list_charts()] + + message = { + 'tests': { + 'passed': [], + 'skipped': [], + 'failed': [] + } + } + + for group in armada_obj.get(const.KEYWORD_ARMADA).get( + const.KEYWORD_GROUPS): + for ch in group.get(const.KEYWORD_CHARTS): + release_name = release_prefix( + prefix, ch.get('chart').get('chart_name')) + + if release_name in known_releases: + self.logger.info('RUNNING: %s tests', release_name) + resp = tiller.testing_release(release_name) + + if not resp: + continue + + test_status = getattr( + resp.info.status, 'last_test_suite_run', + 'FAILED') + if test_status.results[0].status: + self.logger.info("PASSED: %s", release_name) + message['test']['passed'].append(release_name) + else: + self.logger.info("FAILED: %s", release_name) + message['test']['failed'].append(release_name) + else: + self.logger.info( + 'Release %s not found - SKIPPING', release_name) + message['test']['skipped'].append(release_name) + + resp.status = falcon.HTTP_200 + + resp.body = json.dumps(message) + resp.content_type = 'application/json' + + except Exception as e: + err_message = 'Failed to test manifest: {}'.format(e) + self.error(req.context, err_message) + self.return_error( + resp, falcon.HTTP_500, message=err_message) diff --git a/armada/api/tiller_controller.py b/armada/api/controller/tiller.py similarity index 58% rename from armada/api/tiller_controller.py rename to armada/api/controller/tiller.py index 16b63958..bdb6f0ea 100644 --- a/armada/api/tiller_controller.py +++ b/armada/api/controller/tiller.py @@ -15,16 +15,11 @@ import json import falcon -from oslo_config import cfg -from oslo_log import log as logging from armada import api from armada.common import policy from armada.handlers.tiller import Tiller -LOG = logging.getLogger(__name__) -CONF = cfg.CONF - class Status(api.BaseResource): @policy.enforce('tiller:get_status') @@ -33,21 +28,27 @@ class Status(api.BaseResource): get tiller status ''' try: - message = {'tiller': Tiller().tiller_status()} + opts = req.params + tiller = Tiller( + tiller_host=opts.get('tiller_host', None), + tiller_port=opts.get('tiller_port', None)) - if message.get('tiller', False): - resp.status = falcon.HTTP_200 - else: - resp.status = falcon.HTTP_503 + message = { + 'tiller': { + 'state': tiller.tiller_status(), + 'version': tiller.tiller_version() + } + } - resp.data = json.dumps(message) + resp.status = falcon.HTTP_200 + resp.body = json.dumps(message) resp.content_type = 'application/json' except Exception as e: - self.error(req.context, "Unable to find resources") + err_message = 'Failed to get Tiller Status: {}'.format(e) + self.error(req.context, err_message) self.return_error( - resp, falcon.HTTP_500, - message="Unable to get status: {}".format(e)) + resp, falcon.HTTP_500, message=err_message) class Release(api.BaseResource): @@ -58,21 +59,23 @@ class Release(api.BaseResource): ''' try: # Get tiller releases - handler = Tiller() + opts = req.params + tiller = Tiller(tiller_host=opts.get('tiller_host', None), + tiller_port=opts.get('tiller_port', None)) releases = {} - for release in handler.list_releases(): + for release in tiller.list_releases(): if not releases.get(release.namespace, None): releases[release.namespace] = [] releases[release.namespace].append(release.name) - resp.data = json.dumps({'releases': releases}) + resp.body = json.dumps({'releases': releases}) resp.content_type = 'application/json' resp.status = falcon.HTTP_200 except Exception as e: - self.error(req.context, "Unable to find resources") + err_message = 'Unable to find Tiller Releases: {}'.format(e) + self.error(req.context, err_message) self.return_error( - resp, falcon.HTTP_500, - message="Unable to find Releases: {}".format(e)) + resp, falcon.HTTP_500, message=err_message) diff --git a/armada/api/validation_controller.py b/armada/api/controller/validation.py similarity index 63% rename from armada/api/validation_controller.py rename to armada/api/controller/validation.py index 88f88cd3..7e643296 100644 --- a/armada/api/validation_controller.py +++ b/armada/api/controller/validation.py @@ -13,17 +13,13 @@ # limitations under the License. import json -import yaml import falcon -from oslo_log import log as logging from armada import api from armada.common import policy from armada.utils.lint import validate_armada_documents -LOG = logging.getLogger(__name__) - class Validate(api.BaseResource): ''' @@ -33,24 +29,19 @@ class Validate(api.BaseResource): @policy.enforce('armada:validate_manifest') def on_post(self, req, resp): try: + manifest = self.req_yaml(req) + documents = list(manifest) message = { - 'valid': - validate_armada_documents( - list(yaml.safe_load_all(self.req_json(req)))) + 'valid': validate_armada_documents(documents) } - if message.get('valid', False): - resp.status = falcon.HTTP_200 - else: - resp.status = falcon.HTTP_400 - - resp.data = json.dumps(message) + resp.status = falcon.HTTP_200 + resp.body = json.dumps(message) resp.content_type = 'application/json' except Exception: - self.error(req.context, "Failed: Invalid Armada Manifest") + err_message = 'Failed to validate Armada Manifest' + self.error(req.context, err_message) self.return_error( - resp, - falcon.HTTP_400, - message="Failed: Invalid Armada Manifest") + resp, falcon.HTTP_400, message=err_message) diff --git a/armada/api/middleware.py b/armada/api/middleware.py index bc0cf0d6..41b440a4 100644 --- a/armada/api/middleware.py +++ b/armada/api/middleware.py @@ -17,18 +17,20 @@ from uuid import UUID from oslo_config import cfg from oslo_log import log as logging -LOG = logging.getLogger(__name__) CONF = cfg.CONF class AuthMiddleware(object): + def __init__(self): + self.logger = logging.getLogger(__name__) + # Authentication def process_request(self, req, resp): ctx = req.context for k, v in req.headers.items(): - LOG.debug("Request with header %s: %s" % (k, v)) + self.logger.debug("Request with header %s: %s" % (k, v)) auth_status = req.get_header('X-SERVICE-IDENTITY-STATUS') service = True @@ -65,8 +67,9 @@ class AuthMiddleware(object): else: ctx.is_admin_project = False - LOG.debug('Request from authenticated user %s with roles %s' % - (ctx.user, ','.join(ctx.roles))) + self.logger.debug( + 'Request from authenticated user %s with roles %s' % + (ctx.user, ','.join(ctx.roles))) else: ctx.authenticated = False @@ -91,6 +94,9 @@ class ContextMiddleware(object): class LoggingMiddleware(object): + def __init__(self): + self.logger = logging.getLogger(__name__) + def process_response(self, req, resp, resource, req_succeeded): ctx = req.context extra = { @@ -99,4 +105,4 @@ class LoggingMiddleware(object): 'external_ctx': ctx.external_marker, } resp.append_header('X-Armada-Req', ctx.request_id) - LOG.info("%s - %s" % (req.uri, resp.status), extra=extra) + self.logger.info("%s - %s" % (req.uri, resp.status), extra=extra) diff --git a/armada/api/server.py b/armada/api/server.py index cdc76d9f..b8d34da9 100644 --- a/armada/api/server.py +++ b/armada/api/server.py @@ -12,34 +12,28 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os - import falcon from oslo_config import cfg -from oslo_log import log as logging from armada import conf from armada.api import ArmadaRequest -from armada.api.armada_controller import Apply +from armada.api.controller.armada import Apply from armada.api.middleware import AuthMiddleware from armada.api.middleware import ContextMiddleware from armada.api.middleware import LoggingMiddleware -from armada.api.tiller_controller import Release -from armada.api.tiller_controller import Status -from armada.api.validation_controller import Validate +from armada.api.controller.test import Test +from armada.api.controller.test import Tests +from armada.api.controller.tiller import Release +from armada.api.controller.tiller import Status +from armada.api.controller.validation import Validate from armada.common import policy -LOG = logging.getLogger(__name__) conf.set_app_default_configs() CONF = cfg.CONF # Build API def create(middleware=CONF.middleware): - if not (os.path.exists('etc/armada/armada.conf')): - logging.register_options(CONF) - logging.set_defaults(default_log_levels=CONF.default_log_levels) - logging.setup(CONF, 'armada') policy.setup_policy() @@ -55,13 +49,17 @@ def create(middleware=CONF.middleware): api = falcon.API(request_type=ArmadaRequest) # Configure API routing - url_routes_v1 = (('apply', Apply()), - ('releases', Release()), - ('status', Status()), - ('validate', Validate())) + url_routes_v1 = ( + ('apply', Apply()), + ('releases', Release()), + ('status', Status()), + ('tests', Tests()), + ('test/{release}', Test()), + ('validate', Validate()), + ) for route, service in url_routes_v1: - api.add_route("/v1.0/{}".format(route), service) + api.add_route("/api/v1.0/{}".format(route), service) return api diff --git a/armada/cli/__init__.py b/armada/cli/__init__.py index 3a4dedd8..ee00494a 100644 --- a/armada/cli/__init__.py +++ b/armada/cli/__init__.py @@ -11,3 +11,22 @@ # 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 + +CONF = cfg.CONF + +LOG = logging.getLogger(__name__) + + +class CliAction(object): + + def __init__(self): + self.logger = LOG + logging.register_options(CONF) + logging.set_defaults(default_log_levels=CONF.default_log_levels) + logging.setup(CONF, 'armada') + + def invoke(self): + raise Exception() diff --git a/armada/cli/apply.py b/armada/cli/apply.py index 5501df38..dc96f313 100644 --- a/armada/cli/apply.py +++ b/armada/cli/apply.py @@ -12,61 +12,195 @@ # See the License for the specific language governing permissions and # limitations under the License. -from cliff import command as cmd +import yaml +import click +from oslo_config import cfg + +from armada.cli import CliAction from armada.handlers.armada import Armada - -def applyCharts(args): - - armada = Armada(open(args.file).read(), - args.disable_update_pre, - args.disable_update_post, - args.enable_chart_cleanup, - args.dry_run, - args.set, - args.wait, - args.timeout, - args.tiller_host, - args.tiller_port, - args.values, - args.debug_logging) - armada.sync() +CONF = cfg.CONF -class ApplyChartsCommand(cmd.Command): - def get_parser(self, prog_name): - parser = super(ApplyChartsCommand, self).get_parser(prog_name) - parser.add_argument('file', type=str, metavar='FILE', - help='Armada yaml file') - parser.add_argument('--dry-run', action='store_true', - default=False, help='Run charts with dry run') - parser.add_argument('--debug-logging', action='store_true', - default=False, help='Show debug logs') - parser.add_argument('--disable-update-pre', action='store_true', - default=False, help='Disable pre upgrade actions') - parser.add_argument('--disable-update-post', action='store_true', - default=False, help='Disable post upgrade actions') - parser.add_argument('--enable-chart-cleanup', action='store_true', - default=False, help='Enable Chart Clean Up') - parser.add_argument('--set', action='append', help='Override Armada' - ' manifest values.') - parser.add_argument('--wait', action='store_true', - default=False, help='Wait until all charts' - 'have been deployed') - parser.add_argument('--timeout', action='store', type=int, - default=3600, help='Specifies time to wait' - ' for charts to deploy') - parser.add_argument('--tiller-host', action='store', type=str, - help='Specify the tiller host') +@click.group() +def apply(): + """ Apply manifest to cluster - parser.add_argument('--tiller-port', action='store', type=int, - default=44134, help='Specify the tiller port') + """ - parser.add_argument('--values', action='append', - help='Override manifest values with a yaml file') - return parser +DESC = """ +This command install and updates charts defined in armada manifest - def take_action(self, parsed_args): - applyCharts(parsed_args) +The apply argument must be relative path to Armada Manifest. Executing apply +commnad once will install all charts defined in manifest. Re-executing apply +commnad will execute upgrade. + +To see how to create an Armada manifest: + http://armada-helm.readthedocs.io/en/latest/operations/ + +To obtain install/upgrade charts: + + \b + $ armada apply examples/simple.yaml + +To obtain override manifest: + + \b + $ armada apply examples/simple.yaml \ +--set manifest:simple-armada:relase_name="wordpress" + + \b + or + + \b + $ armada apply examples/simple.yaml \ +--values examples/simple-ovr-values.yaml + +""" + +SHORT_DESC = "command install manifest charts" + + +@apply.command(name='apply', help=DESC, short_help=SHORT_DESC) +@click.argument('filename') +@click.option('--api', help="Contacts service endpoint", is_flag=True) +@click.option( + '--disable-update-post', help="run charts without install", is_flag=True) +@click.option( + '--disable-update-pre', help="run charts without install", is_flag=True) +@click.option('--dry-run', help="run charts without install", is_flag=True) +@click.option( + '--enable-chart-cleanup', help="Clean up Unmanaged Charts", is_flag=True) +@click.option('--set', multiple=True, type=str, default=[]) +@click.option('--tiller-host', help="Tiller host ip") +@click.option( + '--tiller-port', help="Tiller host port", type=int, default=44134) +@click.option( + '--timeout', help="specifies time to wait for charts", type=int, + default=3600) +@click.option('--values', '-f', multiple=True, type=str, default=[]) +@click.option( + '--wait', help="wait until all charts deployed", is_flag=True) +@click.option( + '--debug/--no-debug', help='Enable or disable debugging', default=False) +@click.pass_context +def apply_create(ctx, + filename, + api, + disable_update_post, + disable_update_pre, + dry_run, + enable_chart_cleanup, + set, + tiller_host, + tiller_port, + timeout, + values, + wait, + debug): + + if debug: + CONF.debug = debug + + ApplyManifest( + ctx, + filename, + api, + disable_update_post, + disable_update_pre, + dry_run, + enable_chart_cleanup, + set, + tiller_host, + tiller_port, + timeout, + values, + wait).invoke() + + +class ApplyManifest(CliAction): + def __init__(self, + ctx, + filename, + api, + disable_update_post, + disable_update_pre, + dry_run, + enable_chart_cleanup, + set, + tiller_host, + tiller_port, + timeout, + values, + wait): + super(ApplyManifest, self).__init__() + self.ctx = ctx + self.filename = filename + self.api = api + self.disable_update_post = disable_update_post + self.disable_update_pre = disable_update_pre + self.dry_run = dry_run + self.enable_chart_cleanup = enable_chart_cleanup + self.set = set + self.tiller_host = tiller_host + self.tiller_port = tiller_port + self.timeout = timeout + self.values = values + self.wait = wait + + def output(self, resp): + for result in resp: + if not resp[result] and not result == 'diff': + self.logger.info( + 'Did not performed chart %s(s)', result) + elif result == 'diff' and not resp[result]: + self.logger.info('No Relase changes detected') + + for ch in resp[result]: + if not result == 'diff': + msg = 'Chart {} was {}'.format(ch, result) + self.logger.info(msg) + else: + self.logger.info('Chart values diff') + self.logger.info(ch) + + def invoke(self): + + if not self.ctx.obj.get('api', False): + with open(self.filename) as f: + armada = Armada( + list(yaml.safe_load_all(f.read())), + self.disable_update_pre, + self.disable_update_post, + self.enable_chart_cleanup, + self.dry_run, + self.set, + self.wait, + self.timeout, + self.tiller_host, + self.tiller_port, + self.values) + + resp = armada.sync() + self.output(resp) + else: + query = { + 'disable_update_post': self.disable_update_post, + 'disable_update_pre': self.disable_update_pre, + 'dry_run': self.dry_run, + 'enable_chart_cleanup': self.enable_chart_cleanup, + 'tiller_host': self.tiller_host, + 'tiller_port': self.tiller_port, + 'timeout': self.timeout, + 'wait': self.wait + } + + client = self.ctx.obj.get('CLIENT') + + with open(self.filename, 'r') as f: + resp = client.post_apply( + manifest=f.read(), values=self.values, set=self.set, + query=query) + self.output(resp.get('message')) diff --git a/armada/cli/test.py b/armada/cli/test.py index 31471756..29fd12c2 100644 --- a/armada/cli/test.py +++ b/armada/cli/test.py @@ -14,94 +14,141 @@ import yaml -from cliff import command as cmd -from oslo_config import cfg -from oslo_log import log as logging +import click +from armada.cli import CliAction from armada import const from armada.handlers.manifest import Manifest from armada.handlers.tiller import Tiller from armada.utils.release import release_prefix -LOG = logging.getLogger(__name__) -CONF = cfg.CONF +@click.group() +def test(): + """ Test Manifest Charts + + """ -def testService(args): +DESC = """ +This command test deployed charts - tiller = Tiller(tiller_host=args.tiller_host, tiller_port=args.tiller_port) - known_release_names = [release[0] for release in tiller.list_charts()] +The tiller command uses flags to obtain information from tiller services. +The test command will run the release chart tests either via a the manifest or +by targetings a relase. - if args.release: - LOG.info("RUNNING: %s tests", args.release) - resp = tiller.testing_release(args.release) +To test armada deployed releases: - if not resp: - LOG.info("FAILED: %s", args.release) - return + $ armada test --file examples/simple.yaml - test_status = getattr(resp.info.status, 'last_test_suite_run', - 'FAILED') - if test_status.results[0].status: - LOG.info("PASSED: %s", args.release) - else: - LOG.info("FAILED: %s", args.release) +To test release: - if args.file: - documents = yaml.safe_load_all(open(args.file).read()) - armada_obj = Manifest(documents).get_manifest() - prefix = armada_obj.get(const.KEYWORD_ARMADA).get(const.KEYWORD_PREFIX) + $ armada test --release blog-1 - for group in armada_obj.get(const.KEYWORD_ARMADA).get( - const.KEYWORD_GROUPS): - for ch in group.get(const.KEYWORD_CHARTS): - release_name = release_prefix( - prefix, ch.get('chart').get('chart_name')) +""" - if release_name in known_release_names: - LOG.info('RUNNING: %s tests', release_name) - resp = tiller.testing_release(release_name) +SHORT_DESC = "command test releases" - if not resp: - continue - test_status = getattr(resp.info.status, - 'last_test_suite_run', 'FAILED') - if test_status.results[0].status: - LOG.info("PASSED: %s", release_name) - else: - LOG.info("FAILED: %s", release_name) +@test.command(name='test', help=DESC, short_help=SHORT_DESC) +@click.option('--file', help='armada manifest', type=str) +@click.option('--release', help='helm release', type=str) +@click.option('--tiller-host', help="Tiller Host IP") +@click.option( + '--tiller-port', help="Tiller host Port", type=int, default=44134) +@click.pass_context +def test_charts(ctx, file, release, tiller_host, tiller_port): + TestChartManifest( + ctx, file, release, tiller_host, tiller_port).invoke() + +class TestChartManifest(CliAction): + def __init__(self, ctx, file, release, tiller_host, tiller_port): + + super(TestChartManifest, self).__init__() + self.ctx = ctx + self.file = file + self.release = release + self.tiller_host = tiller_host + self.tiller_port = tiller_port + + def invoke(self): + tiller = Tiller( + tiller_host=self.tiller_host, tiller_port=self.tiller_port) + known_release_names = [release[0] for release in tiller.list_charts()] + + if self.release: + if not self.ctx.obj.get('api', False): + self.logger.info("RUNNING: %s tests", self.release) + resp = tiller.testing_release(self.release) + + if not resp: + self.logger.info("FAILED: %s", self.release) + return + + test_status = getattr(resp.info.status, 'last_test_suite_run', + 'FAILED') + if test_status.results[0].status: + self.logger.info("PASSED: %s", self.release) else: - LOG.info('Release %s not found - SKIPPING', release_name) + self.logger.info("FAILED: %s", self.release) + else: + client = self.ctx.obj.get('CLIENT') + query = { + 'tiller_host': self.tiller_host, + 'tiller_port': self.tiller_port + } + resp = client.get_test_release(release=self.release, + query=query) + self.logger.info(resp.get('result')) + self.logger.info(resp.get('message')) -class TestServerCommand(cmd.Command): - def get_parser(self, prog_name): - parser = super(TestServerCommand, self).get_parser(prog_name) - parser.add_argument( - '--release', action='store', help='testing Helm in Release') - parser.add_argument( - '-f', - '--file', - type=str, - metavar='FILE', - help='testing Helm releases in Manifest') - parser.add_argument( - '--tiller-host', - action='store', - type=str, - default=None, - help='Specify the tiller host') - parser.add_argument( - '--tiller-port', - action='store', - type=int, - default=44134, - help='Specify the tiller port') + if self.file: + if not self.ctx.obj.get('api', False): + documents = yaml.safe_load_all(open(self.file).read()) + armada_obj = Manifest(documents).get_manifest() + prefix = armada_obj.get(const.KEYWORD_ARMADA).get( + const.KEYWORD_PREFIX) - return parser + for group in armada_obj.get(const.KEYWORD_ARMADA).get( + const.KEYWORD_GROUPS): + for ch in group.get(const.KEYWORD_CHARTS): + release_name = release_prefix( + prefix, ch.get('chart').get('chart_name')) - def take_action(self, parsed_args): - testService(parsed_args) + if release_name in known_release_names: + self.logger.info('RUNNING: %s tests', release_name) + resp = tiller.testing_release(release_name) + + if not resp: + continue + + test_status = getattr( + resp.info.status, 'last_test_suite_run', + 'FAILED') + if test_status.results[0].status: + self.logger.info("PASSED: %s", release_name) + else: + self.logger.info("FAILED: %s", release_name) + + else: + self.logger.info( + 'Release %s not found - SKIPPING', + release_name) + else: + client = self.ctx.obj.get('CLIENT') + query = { + 'tiller_host': self.tiller_host, + 'tiller_port': self.tiller_port + } + + with open(self.filename, 'r') as f: + resp = client.get_test_manifest(manifest=f.read(), + query=query) + for test in resp.get('tests'): + self.logger.info('Test State: %s', test) + for item in test.get('tests').get(test): + self.logger.info(item) + + self.logger.info(resp) diff --git a/armada/cli/tiller.py b/armada/cli/tiller.py index abb82c8d..1d803b55 100644 --- a/armada/cli/tiller.py +++ b/armada/cli/tiller.py @@ -12,41 +12,96 @@ # See the License for the specific language governing permissions and # limitations under the License. -from cliff import command as cmd +import click + +from armada.cli import CliAction from armada.handlers.tiller import Tiller -from oslo_config import cfg -from oslo_log import log as logging -LOG = logging.getLogger(__name__) +@click.group() +def tiller(): + """ Tiller Services actions -CONF = cfg.CONF + """ -def tillerServer(args): +DESC = """ +This command gets tiller information - tiller = Tiller() +The tiller command uses flags to obtain information from tiller services - if args.status: - resp = tiller.tiller_version() - LOG.info('Tiller Service: %s', tiller.tiller_status()) - LOG.info('Tiller Version: %s', getattr(resp.Version, 'sem_ver', False)) +To obtain armada deployed releases: - if args.releases: - for release in tiller.list_releases(): - LOG.info("Release: %s ( namespace= %s )", release.name, - release.namespace) + $ armada tiller --releases + +To obtain tiller service status/information: + + $ armada tiller --status + +""" + +SHORT_DESC = "command gets tiller infromation" -class TillerServerCommand(cmd.Command): - def get_parser(self, prog_name): - parser = super(TillerServerCommand, self).get_parser(prog_name) - parser.add_argument('--status', action='store_true', - default=False, help='Check Tiller service') - parser.add_argument('--releases', action='store_true', - default=False, help='List Tiller Releases') - return parser +@tiller.command(name='tiller', help=DESC, short_help=SHORT_DESC) +@click.option('--tiller-host', help="Tiller host ip", default=None) +@click.option( + '--tiller-port', help="Tiller host port", type=int, default=44134) +@click.option('--releases', help="list of deployed releses", is_flag=True) +@click.option('--status', help="Status of Armada services", is_flag=True) +@click.pass_context +def tiller_service(ctx, tiller_host, tiller_port, releases, status): + TillerServices(ctx, tiller_host, tiller_port, releases, status).invoke() - def take_action(self, parsed_args): - tillerServer(parsed_args) + +class TillerServices(CliAction): + + def __init__(self, ctx, tiller_host, tiller_port, releases, status): + super(TillerServices, self).__init__() + self.ctx = ctx + self.tiller_host = tiller_host + self.tiller_port = tiller_port + self.releases = releases + self.status = status + + def invoke(self): + + tiller = Tiller( + tiller_host=self.tiller_host, tiller_port=self.tiller_port) + + if self.status: + if not self.ctx.obj.get('api', False): + self.logger.info('Tiller Service: %s', tiller.tiller_status()) + self.logger.info('Tiller Version: %s', tiller.tiller_version()) + else: + client = self.ctx.obj.get('CLIENT') + query = { + 'tiller_host': self.tiller_host, + 'tiller_port': self.tiller_port + } + resp = client.get_status(query=query) + tiller_status = resp.get('tiller').get('state', False) + tiller_version = resp.get('tiller').get('version') + + self.logger.info("Tiller Service: %s", tiller_status) + self.logger.info("Tiller Version: %s", tiller_version) + + if self.releases: + if not self.ctx.obj.get('api', False): + for release in tiller.list_releases(): + self.logger.info( + "Release %s in namespace: %s", + release.name, release.namespace) + else: + client = self.ctx.obj.get('CLIENT') + query = { + 'tiller_host': self.tiller_host, + 'tiller_port': self.tiller_port + } + resp = client.get_releases(query=query) + for namespace in resp.get('releases'): + for release in resp.get('releases').get(namespace): + self.logger.info( + 'Release %s in namespace: %s', release, + namespace) diff --git a/armada/cli/validate.py b/armada/cli/validate.py index fd1c09fd..c2c67be6 100644 --- a/armada/cli/validate.py +++ b/armada/cli/validate.py @@ -12,39 +12,68 @@ # See the License for the specific language governing permissions and # limitations under the License. -from cliff import command as cmd + +import click import yaml -from armada.utils.lint import validate_armada_documents, validate_armada_object +from armada.cli import CliAction +from armada.utils.lint import validate_armada_documents +from armada.utils.lint import validate_armada_object from armada.handlers.manifest import Manifest -from oslo_config import cfg -from oslo_log import log as logging -LOG = logging.getLogger(__name__) +@click.group() +def validate(): + """ Test Manifest Charts -CONF = cfg.CONF + """ -def validateYaml(args): - documents = yaml.safe_load_all(open(args.file).read()) - manifest_obj = Manifest(documents).get_manifest() - obj_check = validate_armada_object(manifest_obj) - doc_check = validate_armada_documents(documents) +DESC = """ +This command validates Armada Manifest - try: - if doc_check and obj_check: - LOG.info('Successfully validated: %s', args.file) - except Exception: - raise Exception('Failed to validate: %s', args.file) +The validate argument must be a relative path to Armada manifest + + $ armada validate examples/simple.yaml + +""" + +SHORT_DESC = "command validates Armada Manifest" -class ValidateYamlCommand(cmd.Command): - def get_parser(self, prog_name): - parser = super(ValidateYamlCommand, self).get_parser(prog_name) - parser.add_argument('file', type=str, metavar='FILE', - help='Armada yaml file to validate') - return parser +@validate.command(name='validate', help=DESC, short_help=SHORT_DESC) +@click.argument('filename') +@click.pass_context +def validate_manifest(ctx, filename): + ValidateManifest(ctx, filename).invoke() - def take_action(self, parsed_args): - validateYaml(parsed_args) + +class ValidateManifest(CliAction): + + def __init__(self, ctx, filename): + super(ValidateManifest, self).__init__() + self.ctx = ctx + self.filename = filename + + def invoke(self): + if not self.ctx.obj.get('api', False): + documents = yaml.safe_load_all(open(self.filename).read()) + manifest_obj = Manifest(documents).get_manifest() + obj_check = validate_armada_object(manifest_obj) + doc_check = validate_armada_documents(documents) + + try: + if doc_check and obj_check: + self.logger.info( + 'Successfully validated: %s', self.filename) + except Exception: + raise Exception('Failed to validate: %s', self.filename) + else: + client = self.ctx.obj.get('CLIENT') + with open(self.filename, 'r') as f: + resp = client.post_validate(f.read()) + if resp.get('valid', False): + self.logger.info( + 'Successfully validated: %s', self.filename) + else: + self.logger.error("Failed to validate: %s", self.filename) diff --git a/armada/common/client.py b/armada/common/client.py new file mode 100644 index 00000000..9853219c --- /dev/null +++ b/armada/common/client.py @@ -0,0 +1,107 @@ +# Copyright 2017 The Armada Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import yaml + +from oslo_config import cfg +from oslo_log import log as logging + +from armada.exceptions import api_exceptions as err +from armada.handlers.armada import Override + +LOG = logging.getLogger(__name__) +CONF = cfg.CONF + +API_VERSION = 'v{}/{}' + + +class ArmadaClient(object): + + def __init__(self, session): + self.session = session + + def _set_endpoint(self, version, action): + return API_VERSION.format(version, action) + + def get_status(self, query): + + endpoint = self._set_endpoint('1.0', 'status') + resp = self.session.get(endpoint, query=query) + + self._check_response(resp) + + return resp.json() + + def get_releases(self, query): + + endpoint = self._set_endpoint('1.0', 'releases') + resp = self.session.get(endpoint, query=query) + + self._check_response(resp) + + return resp.json() + + def post_validate(self, manifest=None): + + endpoint = self._set_endpoint('1.0', 'validate') + resp = self.session.post(endpoint, body=manifest) + + self._check_response(resp) + + return resp.json() + + def post_apply(self, manifest=None, values=None, set=None, query=None): + + if values or set: + document = list(yaml.safe_load_all(manifest)) + override = Override( + document, overrides=set, values=values).update_manifests() + manifest = yaml.dump(override) + + endpoint = self._set_endpoint('1.0', 'apply') + resp = self.session.post(endpoint, body=manifest, query=query) + + self._check_response(resp) + + return resp.json() + + def get_test_release(self, release=None, query=None): + + endpoint = self._set_endpoint('1.0', 'test/{}'.format(release)) + resp = self.session.get(endpoint, query=query) + + self._check_response(resp) + + return resp.json() + + def post_test_manifest(self, manifest=None, query=None): + + endpoint = self._set_endpoint('1.0', 'tests') + resp = self.session.post(endpoint, body=manifest, query=query) + + self._check_response(resp) + + return resp.json() + + def _check_response(self, resp): + if resp.status_code == 401: + raise err.ClientUnauthorizedError( + "Unauthorized access to %s, include valid token.".format( + resp.url)) + elif resp.status_code == 403: + raise err.ClientForbiddenError( + "Forbidden access to %s" % resp.url) + elif not resp.ok: + raise err.ClientError( + "Error - received %d: %s" % (resp.status_code, resp.text)) diff --git a/armada/common/policies/service.py b/armada/common/policies/service.py index c27254fa..5bb7d363 100644 --- a/armada/common/policies/service.py +++ b/armada/common/policies/service.py @@ -20,12 +20,22 @@ armada_policies = [ name=base.ARMADA % 'create_endpoints', check_str=base.RULE_ADMIN_REQUIRED, description='install manifest charts', - operations=[{'path': '/v1.0/apply/', 'method': 'POST'}]), + operations=[{'path': '/api/v1.0/apply/', 'method': 'POST'}]), policy.DocumentedRuleDefault( name=base.ARMADA % 'validate_manifest', check_str=base.RULE_ADMIN_REQUIRED, + description='validate installed manifest', + operations=[{'path': '/api/v1.0/validate/', 'method': 'POST'}]), + policy.DocumentedRuleDefault( + name=base.ARMADA % 'test_release', + check_str=base.RULE_ADMIN_REQUIRED, description='validate install manifest', - operations=[{'path': '/v1.0/validate/', 'method': 'POST'}]), + operations=[{'path': '/api/v1.0/test/{release}', 'method': 'GET'}]), + policy.DocumentedRuleDefault( + name=base.ARMADA % 'test_manifest', + check_str=base.RULE_ADMIN_REQUIRED, + description='validate install manifest', + operations=[{'path': '/api/v1.0/tests/', 'method': 'POST'}]), ] diff --git a/armada/common/policies/tiller.py b/armada/common/policies/tiller.py index d6e04131..6370fdcb 100644 --- a/armada/common/policies/tiller.py +++ b/armada/common/policies/tiller.py @@ -20,15 +20,13 @@ tiller_policies = [ name=base.TILLER % 'get_status', check_str=base.RULE_ADMIN_REQUIRED, description='Get tiller status', - operations=[{'path': '/v1.0/status/', - 'method': 'GET'}]), + operations=[{'path': '/api/v1.0/status/', 'method': 'GET'}]), policy.DocumentedRuleDefault( name=base.TILLER % 'get_release', check_str=base.RULE_ADMIN_REQUIRED, description='Get tiller release', - operations=[{'path': '/v1.0/releases/', - 'method': 'GET'}]), + operations=[{'path': '/api/v1.0/releases/', 'method': 'GET'}]), ] diff --git a/armada/common/session.py b/armada/common/session.py new file mode 100644 index 00000000..558efd33 --- /dev/null +++ b/armada/common/session.py @@ -0,0 +1,96 @@ +# Copyright 2017 The Armada Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import requests + +from oslo_config import cfg +from oslo_log import log as logging + +LOG = logging.getLogger(__name__) +CONF = cfg.CONF + + +class ArmadaSession(object): + """ + A session to the Armada API maintaining credentials and API options + + :param string host: The armada server hostname or IP + :param int port: (optional) The service port appended if specified + :param string token: Auth token + :param string marker: (optional) external context marker + """ + + def __init__(self, host, port=None, scheme='http', token=None, + marker=None): + + self._session = requests.Session() + self._session.headers.update({ + 'X-Auth-Token': token, + 'X-Context-Marker': marker + }) + self.host = host + self.scheme = scheme + + if port: + self.port = port + self.base_url = "{}://{}:{}/api/".format( + self.scheme, self.host, self.port) + else: + self.base_url = "{}://{}/api/".format( + self.scheme, self.host) + + self.token = token + self.marker = marker + self.logger = LOG + + # TODO Add keystone authentication to produce a token for this session + def get(self, endpoint, query=None): + """ + Send a GET request to armada. + + :param string endpoint: URL string following hostname and API prefix + :param dict query: A dict of k, v pairs to add to the query string + + :return: A requests.Response object + """ + api_url = '{}{}'.format(self.base_url, endpoint) + resp = self._session.get( + api_url, params=query, timeout=3600) + + return resp + + def post(self, endpoint, query=None, body=None, data=None): + """ + Send a POST request to armada. If both body and data are specified, + body will will be used. + + :param string endpoint: URL string following hostname and API prefix + :param dict query: dict of k, v parameters to add to the query string + :param string body: string to use as the request body. + :param data: Something json.dumps(s) can serialize. + :return: A requests.Response object + """ + api_url = '{}{}'.format(self.base_url, endpoint) + + self.logger.debug("Sending POST with armada_client session") + if body is not None: + self.logger.debug("Sending POST with explicit body: \n%s" % body) + resp = self._session.post( + api_url, params=query, data=body, timeout=3600) + else: + self.logger.debug("Sending POST with JSON body: \n%s" % str(data)) + resp = self._session.post( + api_url, params=query, json=data, timeout=3600) + + return resp diff --git a/armada/conf/__init__.py b/armada/conf/__init__.py index 11a61288..463cd027 100644 --- a/armada/conf/__init__.py +++ b/armada/conf/__init__.py @@ -17,12 +17,13 @@ import os from oslo_config import cfg from armada.conf import default +from armada import const CONF = cfg.CONF # Load config file if exists -if (os.path.exists('etc/armada/armada.conf')): - CONF(['--config-file', 'etc/armada/armada.conf']) +if (os.path.exists(const.CONFIG_PATH)): + CONF(['--config-file', const.CONFIG_PATH]) def set_app_default_configs(): diff --git a/armada/conf/default.py b/armada/conf/default.py index 303d7961..d7316900 100644 --- a/armada/conf/default.py +++ b/armada/conf/default.py @@ -14,6 +14,8 @@ from oslo_config import cfg +from keystoneauth1 import loading + from armada.conf import utils default_options = [ @@ -71,7 +73,12 @@ The Keystone project domain name used for authentication. def register_opts(conf): conf.register_opts(default_options) + conf.register_opts( + loading.get_auth_plugin_conf_options('password'), + group='keystone_authtoken') def list_opts(): - return {'DEFAULT': default_options} + return { + 'DEFAULT': default_options, + 'keystone_authtoken': loading.get_auth_plugin_conf_options('password')} diff --git a/armada/const.py b/armada/const.py index a3f280aa..a473f69f 100644 --- a/armada/const.py +++ b/armada/const.py @@ -28,3 +28,6 @@ KEYWORD_CHART = 'chart' # Statuses STATUS_DEPLOYED = 'DEPLOYED' STATUS_FAILED = 'FAILED' + +# Configuration File +CONFIG_PATH = '/etc/armada/armada.conf' diff --git a/armada/exceptions/api_exceptions.py b/armada/exceptions/api_exceptions.py index ffec860e..7be8a8b9 100644 --- a/armada/exceptions/api_exceptions.py +++ b/armada/exceptions/api_exceptions.py @@ -31,3 +31,21 @@ class ApiJsonException(ApiException): '''Exception that occurs during chart cleanup.''' message = 'There was an error listing the helm chart releases.' + + +class ClientUnauthorizedError(ApiException): + '''Exception that occurs during chart cleanup.''' + + message = 'There was an error listing the helm chart releases.' + + +class ClientForbiddenError(ApiException): + '''Exception that occurs during chart cleanup.''' + + message = 'There was an error listing the helm chart releases.' + + +class ClientError(ApiException): + '''Exception that occurs during chart cleanup.''' + + message = 'There was an error listing the helm chart releases.' diff --git a/armada/handlers/armada.py b/armada/handlers/armada.py index 66cabf4c..57aadc57 100644 --- a/armada/handlers/armada.py +++ b/armada/handlers/armada.py @@ -53,8 +53,7 @@ class Armada(object): timeout=DEFAULT_TIMEOUT, tiller_host=None, tiller_port=44134, - values=None, - debug=False): + values=None): ''' Initialize the Armada Engine and establish a connection to Tiller @@ -69,14 +68,8 @@ class Armada(object): self.timeout = timeout self.tiller = Tiller(tiller_host=tiller_host, tiller_port=tiller_port) self.values = values - self.documents = list(yaml.safe_load_all(file)) + self.documents = file self.config = None - self.debug = debug - - # Set debug value - # Define a default handler at INFO logging level - if self.debug: - logging.basicConfig(level=logging.DEBUG) def get_armada_manifest(self): return Manifest(self.documents).get_manifest() @@ -193,7 +186,7 @@ class Armada(object): Syncronize Helm with the Armada Config(s) ''' - msg = {'installed': [], 'upgraded': [], 'diff': []} + msg = {'install': [], 'upgrade': [], 'diff': []} # TODO: (gardlt) we need to break up this func into # a more cleaner format @@ -314,7 +307,7 @@ class Armada(object): timeout=wait_values.get('timeout', DEFAULT_TIMEOUT) ) - msg['upgraded'].append(prefix_chart) + msg['upgrade'].append(prefix_chart) # process install else: @@ -338,7 +331,7 @@ class Armada(object): namespace=chart.namespace, timeout=wait_values.get('timeout', 3600)) - msg['installed'].append(prefix_chart) + msg['install'].append(prefix_chart) LOG.debug("Cleaning up chart source in %s", chartbuilder.source_directory) diff --git a/armada/handlers/k8s.py b/armada/handlers/k8s.py index 03413ffb..14920c47 100644 --- a/armada/handlers/k8s.py +++ b/armada/handlers/k8s.py @@ -15,7 +15,9 @@ import re import time -from kubernetes import client, config, watch +from kubernetes import client +from kubernetes import config +from kubernetes import watch from kubernetes.client.rest import ApiException from oslo_config import cfg from oslo_log import log as logging @@ -37,7 +39,10 @@ class K8s(object): ''' Initialize connection to Kubernetes ''' - config.load_kube_config() + try: + config.load_incluster_config() + except: + config.load_kube_config() self.client = client.CoreV1Api() self.batch_api = client.BatchV1Api() diff --git a/armada/handlers/tiller.py b/armada/handlers/tiller.py index ec898f9c..c6decd35 100644 --- a/armada/handlers/tiller.py +++ b/armada/handlers/tiller.py @@ -309,9 +309,6 @@ class Tiller(object): LOG.info("Wait: %s, Timeout: %s", wait, timeout) - if timeout > self.timeout: - self.timeout = timeout - if values is None: values = Config(raw='') else: @@ -349,8 +346,9 @@ class Tiller(object): try: stub = ReleaseServiceStub(self.channel) - release_request = TestReleaseRequest(name=release, timeout=timeout, - cleanup=cleanup) + + release_request = TestReleaseRequest( + name=release, timeout=timeout, cleanup=cleanup) content = self.get_release_content(release) @@ -417,9 +415,11 @@ class Tiller(object): stub = ReleaseServiceStub(self.channel) release_request = GetVersionRequest() - return stub.GetVersion( + tiller_version = stub.GetVersion( release_request, self.timeout, metadata=self.metadata) + return getattr(tiller_version.Version, 'sem_ver', None) + except Exception: raise ex.TillerVersionException() diff --git a/armada/shell.py b/armada/shell.py index 276737cd..11d67bf6 100644 --- a/armada/shell.py +++ b/armada/shell.py @@ -12,39 +12,81 @@ # See the License for the specific language governing permissions and # limitations under the License. -import sys +from urllib.parse import urlparse +import click from oslo_config import cfg from oslo_log import log -from cliff import app -from cliff import commandmanager as cm -import armada +from armada.cli.apply import apply_create +from armada.cli.test import test_charts +from armada.cli.tiller import tiller_service +from armada.cli.validate import validate_manifest +from armada.common.client import ArmadaClient +from armada.common.session import ArmadaSession CONF = cfg.CONF -class ArmadaApp(app.App): - def __init__(self, **kwargs): - super(ArmadaApp, self).__init__( - description='Armada - Upgrade and deploy your charts', - version=armada.__version__, - command_manager=cm.CommandManager('armada'), - **kwargs) +@click.group() +@click.option( + '--debug/--no-debug', help='Enable or disable debugging', default=False) +@click.option( + '--api/--no-api', help='Execute service endpoints. (requires url option)', + default=False) +@click.option( + '--url', help='Armada Service Endpoint', envvar='HOST', default=None) +@click.option( + '--token', help='Keystone Service Token', envvar='TOKEN', default=None) +@click.pass_context +def main(ctx, debug, api, url, token): + """ + Multi Helm Chart Deployment Manager - def build_option_parser(self, description, version, argparse_kwargs=None): - parser = super(ArmadaApp, self).build_option_parser( - description, version, argparse_kwargs) - return parser + Common actions from this point include: - def configure_logging(self): - super(ArmadaApp, self).configure_logging() - log.register_options(CONF) - log.set_defaults(default_log_levels=CONF.default_log_levels) - log.setup(CONF, 'armada') + \b + $ armada apply + $ armada test + $ armada tiller + $ armada validate + + Environment: + + \b + $TOKEN set auth token + $HOST set armada service host endpoint + + This tool will communicate with deployed Tiller in your Kubernetes cluster. + """ + + if not ctx.obj: + ctx.obj = {} + + if api: + if not url or not token: + raise click.ClickException( + 'When api option is enable user needs to pass url') + else: + ctx.obj['api'] = api + parsed_url = urlparse(url) + ctx.obj['CLIENT'] = ArmadaClient( + ArmadaSession( + host=parsed_url.netloc, + scheme=parsed_url.scheme, + token=token) + ) + + log.register_options(CONF) + + if debug: + CONF.debug = debug + + log.set_defaults(default_log_levels=CONF.default_log_levels) + log.setup(CONF, 'armada') -def main(argv=None): - if argv is None: - argv = sys.argv[1:] - return ArmadaApp().run(argv) +main.add_command(apply_create) +main.add_command(test_charts) +main.add_command(tiller_service) +main.add_command(validate_manifest) diff --git a/armada/tests/unit/api/test_api.py b/armada/tests/unit/api/test_api.py index ac29abf7..5a749225 100644 --- a/armada/tests/unit/api/test_api.py +++ b/armada/tests/unit/api/test_api.py @@ -38,7 +38,7 @@ class TestAPI(APITestCase): @mock.patch('armada.api.armada_controller.Handler') def test_armada_apply(self, mock_armada): ''' - Test /armada/apply endpoint + Test /api/v1.0/apply endpoint ''' mock_armada.sync.return_value = None @@ -54,7 +54,7 @@ class TestAPI(APITestCase): doc = {u'message': u'Success'} - result = self.simulate_post(path='/armada/apply', body=body) + result = self.simulate_post(path='/api/v1.0/apply', body=body) self.assertEqual(result.json, doc) @unittest.skip('Test does not handle auth/policy correctly') @@ -62,6 +62,7 @@ class TestAPI(APITestCase): def test_tiller_status(self, mock_tiller): ''' Test /status endpoint + Test /api/v1.0/status endpoint ''' # Mock tiller status value @@ -70,11 +71,13 @@ class TestAPI(APITestCase): # FIXME(lamt) This variable is unused. Uncomment when it is. # doc = {u'message': u'Tiller Server is Active'} - result = self.simulate_get('/v1.0/status') + result = self.simulate_get('/api/v1.0/status') # TODO(lamt) This should be HTTP_401 if no auth is happening, but auth # is not implemented currently, so it falls back to a policy check # failure, thus a 403. Change this once it is completed + + # Fails due to invalid access self.assertEqual(falcon.HTTP_403, result.status) # FIXME(lamt) Need authentication - mock, fixture @@ -84,7 +87,7 @@ class TestAPI(APITestCase): @mock.patch('armada.api.tiller_controller.Tiller') def test_tiller_releases(self, mock_tiller): ''' - Test /tiller/releases endpoint + Test /api/v1.0/releases endpoint ''' # Mock tiller status value @@ -93,7 +96,7 @@ class TestAPI(APITestCase): # FIXME(lamt) This variable is unused. Uncomment when it is. # doc = {u'releases': {}} - result = self.simulate_get('/v1.0/releases') + result = self.simulate_get('/api/v1.0/releases') # TODO(lamt) This should be HTTP_401 if no auth is happening, but auth # is not implemented currently, so it falls back to a policy check diff --git a/armada/tests/unit/test_policy.py b/armada/tests/unit/common/test_policy.py similarity index 100% rename from armada/tests/unit/test_policy.py rename to armada/tests/unit/common/test_policy.py diff --git a/docs/source/commands/apply.rst b/docs/source/commands/apply.rst index ab6accd4..6e600270 100644 --- a/docs/source/commands/apply.rst +++ b/docs/source/commands/apply.rst @@ -7,15 +7,42 @@ Commands .. code:: bash - Usage: armada apply FILE + Usage: armada apply [OPTIONS] FILENAME + This command install and updates charts defined in armada manifest + + The apply argument must be relative path to Armada Manifest. Executing + apply commnad once will install all charts defined in manifest. Re- + executing apply commnad will execute upgrade. + + To see how to create an Armada manifest: + http://armada-helm.readthedocs.io/en/latest/operations/ + + To obtain install/upgrade charts: + + $ armada apply examples/simple.yaml + + To obtain override manifest: + + $ armada apply examples/simple.yaml --set manifest:simple-armada:relase_name="wordpress" + + or + + $ armada apply examples/simple.yaml --values examples/simple-ovr-values.yaml Options: - - [-h] [--dry-run] [--debug-logging] [--disable-update-pre] - [--disable-update-post] [--enable-chart-cleanup] [--wait] - [--timeout TIMEOUT] - + --api Contacts service endpoint + --disable-update-post run charts without install + --disable-update-pre run charts without install + --dry-run run charts without install + --enable-chart-cleanup Clean up Unmanaged Charts + --set TEXT + --tiller-host TEXT Tiller host ip + --tiller-port INTEGER Tiller host port + --timeout INTEGER specifies time to wait for charts + -f, --values TEXT + --wait wait until all charts deployed + --help Show this message and exit. Synopsis -------- diff --git a/docs/source/commands/test.rst b/docs/source/commands/test.rst index 345dd246..8f08aa06 100644 --- a/docs/source/commands/test.rst +++ b/docs/source/commands/test.rst @@ -7,11 +7,28 @@ Commands .. code:: bash - Usage: armada test + Usage: armada test [OPTIONS] + + This command test deployed charts + + The tiller command uses flags to obtain information from tiller services. + The test command will run the release chart tests either via a + manifest or by targeting a relase. + + To obtain armada deployed releases: + + $ armada test --file examples/simple.yaml + + To test release: + + $ armada test --release blog-1 Options: - - [-h] [--release RELEASE] [--file FILE] + --file TEXT armada manifest + --release TEXT helm release + --tiller-host TEXT Tiller Host IP + --tiller-port INTEGER Tiller host Port + --help Show this message and exit. Synopsis diff --git a/docs/source/commands/tiller.rst b/docs/source/commands/tiller.rst index 70baded0..d4608176 100644 --- a/docs/source/commands/tiller.rst +++ b/docs/source/commands/tiller.rst @@ -7,12 +7,26 @@ Commands .. code:: bash - Usage: armada tiller + Usage: armada tiller [OPTIONS] + + This command gets tiller information + + The tiller command uses flags to obtain information from tiller services + + To obtain armada deployed releases: + + $ armada tiller --releases + + To obtain tiller service status/information: + + $ armada tiller --status Options: - - [-h] [--status] [--releases] - + --tiller-host TEXT Tiller host ip + --tiller-port INTEGER Tiller host port + --releases list of deployed releases + --status Status of Armada services + --help Show this message and exit. Synopsis -------- diff --git a/docs/source/commands/validate.rst b/docs/source/commands/validate.rst index 74c51ea4..cb4c2ed6 100644 --- a/docs/source/commands/validate.rst +++ b/docs/source/commands/validate.rst @@ -7,11 +7,16 @@ Commands .. code:: bash - Usage: armada validate FILE + Usage: armada validate [OPTIONS] FILENAME + + This command validates Armada Manifest + + The validate argument must be a relative path to Armada manifest + + $ armada validate examples/simple.yaml Options: - - [-h] + --help Show this message and exit. Synopsis -------- diff --git a/docs/source/development/getting-started.rst b/docs/source/development/getting-started.rst index 29da80ad..0fedfd27 100644 --- a/docs/source/development/getting-started.rst +++ b/docs/source/development/getting-started.rst @@ -13,8 +13,7 @@ To use the docker containter to develop: .. code-block:: bash - git clone http://github.com/att-comdev/armada.git - cd armada + git clone http://github.com/att-comdev/armada.git && cd armada pip install tox @@ -23,7 +22,7 @@ To use the docker containter to develop: docker build . -t armada/latest - docker run -d --name armada -v ~/.kube/config:/armada/.kube/config -v $(pwd)/etc:/armada/etc armada:local + docker run -d --name armada -v ~/.kube/:/armada/.kube/ -v $(pwd)/etc:/etc armada:local .. note:: @@ -45,7 +44,8 @@ From the directory of the forked repository: git clone http://github.com/att-comdev/armada.git && cd armada - virtualenv venv + + virtualenv -p python3 venv pip install -r requirements.txt -r test-requirements.txt @@ -53,6 +53,7 @@ From the directory of the forked repository: # Testing your armada code # The tox command will execute lint, bandit, cover + pip install tox tox # For targeted test @@ -60,7 +61,6 @@ From the directory of the forked repository: tox -e bandit tox -e cover - # policy and config are used in order to use and configure Armada API tox -e genconfig tox -e genpolicy diff --git a/docs/source/operations/guide-api.rst b/docs/source/operations/guide-api.rst index 91e4e9b8..b44cf649 100644 --- a/docs/source/operations/guide-api.rst +++ b/docs/source/operations/guide-api.rst @@ -1,87 +1,532 @@ -Armada RESTful API -=================== +Armada Restful API v1.0 +======================= -Armada Endpoints +Description +~~~~~~~~~~~ + +The Armada API provides the services similar to the cli via Restful endpoints + + +Base URL +~~~~~~~~ + +https://armada.localhost/api/v1.0/ + +DEFAULT +~~~~~~~ + +GET ``/releases`` ----------------- -:: - Endpoint: POST /armada/apply +Summary ++++++++ - :string file The yaml file to apply - :>json boolean debug Enable debug logging - :>json boolean disable_update_pre - :>json boolean disable_update_post - :>json boolean enable_chart_cleanup - :>json boolean skip_pre_flight - :>json object values Override manifest values - :>json boolean dry_run - :>json boolean wait - :>json float timeout +Get tiller releases -:: - Request: +Request ++++++++ + + +Responses ++++++++++ + +**200** +^^^^^^^ + +obtain all running releases + +**Example:** + +.. code-block:: javascript { - "file": "examples/openstack-helm.yaml", - "options": { - "debug": true, - "disable_update_pre": false, - "disable_update_post": false, - "enable_chart_cleanup": false, - "skip_pre_flight": false, - "dry_run": false, - "wait": false, - "timeout": false + "message": { + "namespace": [ + "armada-release", + "armada-release" + ], + "default": [ + "armada-release", + "armada-release" + ] } } -:: +**403** +^^^^^^^ - Results: +Unable to Authorize or Permission + + +**405** +^^^^^^^ + +Failed to perform action + +GET ``/status`` +--------------- + + +Summary ++++++++ + +Get armada running state + + +Request ++++++++ + + +Responses ++++++++++ + +**200** +^^^^^^^ + +obtain armada status + +**Example:** + +.. code-block:: javascript { - "message": "success" - } - -Tiller Endpoints ------------------ - -:: - - Endpoint: GET /tiller/releases - - Description: Retrieves tiller releases. - - -:: - - Results: - - { - "releases": { - "armada-memcached": "openstack", - "armada-etcd": "openstack", - "armada-keystone": "openstack", - "armada-rabbitmq": "openstack", - "armada-horizon": "openstack" + "message": { + "tiller": { + "state": True, + "version": "v2.5.0" + } } } +**403** +^^^^^^^ -:: - - Endpoint: GET /tiller/status - - Retrieves the status of the Tiller server. +Unable to Authorize or Permission -:: +**405** +^^^^^^^ - Results: +Failed to perform action + + +GET ``/validate`` +----------------- + + +Summary ++++++++ + +Get tiller releases + + +Request ++++++++ + + +Responses ++++++++++ + +**200** +^^^^^^^ + +obtain all running releases + + +**Example:** + +.. code-block:: javascript { - "message": Tiller Server is Active + "valid": true } + +**403** +^^^^^^^ + +Unable to Authorize or Permission + + +**405** +^^^^^^^ + +Failed to perform action + + +POST ``/apply`` +--------------- + + +Summary ++++++++ + +Install/Update Armada Manifest + +Request ++++++++ + +Body +^^^^ + +.. csv-table:: + :delim: | + :header: "Name", "Required", "Type", "Format", "Properties", "Description" + :widths: 20, 10, 15, 15, 30, 25 + + disable-update-post | boolean | | | | + disable-update-pre | boolean | | | | + dry-run | boolean | | | | + enable-chart-cleanup | boolean | | | | + tiller-host | string | | | | + tiller-port | int | | | | + timeout | int | | | | + wait | boolean | | | | + + +**Armada schema:** + +.. code-block:: javascript + + { + "api": true, + "armada": {} + } + +Responses ++++++++++ + +**200** +^^^^^^^ + +Succesfull installation/update of manifest + +**Example:** + +.. code-block:: javascript + + { + "message": { + "installed": [ + "armada-release", + "armada-release" + ], + "updated": [ + "armada-release", + "armada-release" + ], + "diff": [ + "values": "value diff", + "values": "value diff 2" + ] + } + } + +**403** +^^^^^^^ + +Unable to Authorize or Permission + + +**405** +^^^^^^^ + +Failed to perform action + + +POST ``/test/{release}`` +------------------------ + + +Summary ++++++++ + +Test release name + + +Parameters +++++++++++ + +.. csv-table:: + :delim: | + :header: "Name", "Located in", "Required", "Type", "Format", "Properties", "Description" + :widths: 20, 15, 10, 10, 10, 20, 30 + + release | path | Yes | string | | | name of the release to test + +Request ++++++++ + + +Responses ++++++++++ + +**200** +^^^^^^^ + +Succesfully Test release response + +**Example:** + +.. code-block:: javascript + + { + "message": { + "message": "armada-release", + "result": "No test found." + } + } + +**403** +^^^^^^^ + +Unable to Authorize or Permission + + +**405** +^^^^^^^ + +Failed to perform action + +POST ``/tests`` +--------------- + + +Summary ++++++++ + +Test manifest releases + +Request ++++++++ + +Body +^^^^ + +.. csv-table:: + :delim: | + :header: "Name", "Required", "Type", "Format", "Properties", "Description" + :widths: 20, 10, 15, 15, 30, 25 + + armada | Yes | | | | + +**Armada schema:** + +.. code-block:: javascript + + { + "armada": {} + } + +Responses ++++++++++ + +**200** +^^^^^^^ + +Succesfully Test of manifest + +**Example:** + +.. code-block:: javascript + + { + "message": { + "failed": [ + "armada-release", + "armada-release" + ], + "passed": [ + "armada-release", + "armada-release" + ], + "skipped": [ + "armada-release", + "armada-release" + ] + } + } + +**403** +^^^^^^^ + +Unable to Authorize or Permission + + +**405** +^^^^^^^ + +Failed to perform action + +Data Structures +~~~~~~~~~~~~~~~ + +Armada Request Model Structure +------------------------------ + +.. csv-table:: + :delim: | + :header: "Name", "Required", "Type", "Format", "Properties", "Description" + :widths: 20, 10, 15, 15, 30, 25 + + disable-update-post | boolean | | | | + disable-update-pre | boolean | | | | + dry-run | boolean | | | | + enable-chart-cleanup | boolean | | | | + tiller-host | string | | | | + tiller-port | int | | | | + timeout | int | | | | + wait | boolean | | | | + +**Armada schema:** + +Armada Response Model Structure +------------------------------- + +.. csv-table:: + :delim: | + :header: "Name", "Required", "Type", "Format", "Properties", "Description" + :widths: 20, 10, 15, 15, 30, 25 + + message | No | | | | + +**Message schema:** + +.. csv-table:: + :delim: | + :header: "Name", "Required", "Type", "Format", "Properties", "Description" + :widths: 20, 10, 15, 15, 30, 25 + + installed | No | array of string | | | + updated | No | array of string | | | + values | No | array of string | | | + + +Releases Response Model Structure +--------------------------------- + +.. csv-table:: + :delim: | + :header: "Name", "Required", "Type", "Format", "Properties", "Description" + :widths: 20, 10, 15, 15, 30, 25 + + message | No | | | | + +**Message schema:** + + +.. csv-table:: + :delim: | + :header: "Name", "Required", "Type", "Format", "Properties", "Description" + :widths: 20, 10, 15, 15, 30, 25 + + namespace | No | array of string | | | + +Status Response Model Structure +------------------------------- + +.. csv-table:: + :delim: | + :header: "Name", "Required", "Type", "Format", "Properties", "Description" + :widths: 20, 10, 15, 15, 30, 25 + + message | No | | | | + + + +**Message schema:** + + +.. csv-table:: + :delim: | + :header: "Name", "Required", "Type", "Format", "Properties", "Description" + :widths: 20, 10, 15, 15, 30, 25 + + tiller | No | | | | + + + +**Tiller schema:** + + +.. csv-table:: + :delim: | + :header: "Name", "Required", "Type", "Format", "Properties", "Description" + :widths: 20, 10, 15, 15, 30, 25 + + state | No | string | | | + version | No | string | | | + + + +Test Response Model Structure +----------------------------- + +.. csv-table:: + :delim: | + :header: "Name", "Required", "Type", "Format", "Properties", "Description" + :widths: 20, 10, 15, 15, 30, 25 + + message | No | | | | + +**Message schema:** + + +.. csv-table:: + :delim: | + :header: "Name", "Required", "Type", "Format", "Properties", "Description" + :widths: 20, 10, 15, 15, 30, 25 + + message | No | string | | | + result | No | string | | | + +Tests Request Model Structure +----------------------------- + +.. csv-table:: + :delim: | + :header: "Name", "Required", "Type", "Format", "Properties", "Description" + :widths: 20, 10, 15, 15, 30, 25 + + armada | Yes | | | | + + + +**Armada schema:** + + +Tests Response Model Structure +------------------------------ + +.. csv-table:: + :delim: | + :header: "Name", "Required", "Type", "Format", "Properties", "Description" + :widths: 20, 10, 15, 15, 30, 25 + + message | No | | | | + + +**Message schema:** + + +.. csv-table:: + :delim: | + :header: "Name", "Required", "Type", "Format", "Properties", "Description" + :widths: 20, 10, 15, 15, 30, 25 + + failed | No | array of string | | | + passed | No | array of string | | | + skipped | No | array of string | | | + + +Validate Response Model Structure +--------------------------------- + +.. csv-table:: + :delim: | + :header: "Name", "Required", "Type", "Format", "Properties", "Description" + :widths: 20, 10, 15, 15, 30, 25 + + valid | No | boolean | | | diff --git a/docs/source/operations/guide-configure.rst b/docs/source/operations/guide-configure.rst index fa36ae5f..5629ccd6 100644 --- a/docs/source/operations/guide-configure.rst +++ b/docs/source/operations/guide-configure.rst @@ -9,6 +9,7 @@ file can be generated via tox .. code-block:: bash $ tox -e genconfig + $ tox -e genpolicy Customize your configuration based on the information below @@ -20,9 +21,9 @@ tokens .. note:: - If you do not have a keystone already deploy, then armada can deploy a keystone service. + If you do not have a keystone already deploy, then armada can deploy a keystone services: - armada apply keystone-manifest.yaml + $ armada apply keystone-manifest.yaml .. code-block:: bash @@ -40,13 +41,13 @@ The service account must then be included in the armada.conf .. code-block:: ini [keystone_authtoken] + auth_type = password auth_uri = https://:5000/ + auth_url = https://:35357/ auth_version = 3 delay_auth_decision = true - auth_type = password - auth_url = https://:35357/ - project_name = service - project_domain_name = ucp - user_name = armada - user_domain_name = ucp password = armada + project_domain_name = ucp + project_name = service + user_domain_name = ucp + user_name = armada diff --git a/entrypoint.sh b/entrypoint.sh index a0634282..f8b393d7 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -6,11 +6,7 @@ PORT="8000" set -e if [ "$1" = 'server' ]; then - exec uwsgi --http 0.0.0.0:${PORT} --paste config:$(pwd)/etc/armada/api-paste.ini --enable-threads -L --pyargv " --config-file $(pwd)/etc/armada/armada.conf" -fi - -if [ "$1" = 'tiller' ] || [ "$1" = 'apply' ]; then + exec uwsgi --http :${PORT} --http-timeout 3600 --paste config:/etc/armada/api-paste.ini --enable-threads -L --pyargv "--config-file /etc/armada/armada.conf" +else exec $CMD "$@" fi - -exec "$@" diff --git a/etc/armada/armada.conf.sample b/etc/armada/armada.conf.sample new file mode 100644 index 00000000..eb04949e --- /dev/null +++ b/etc/armada/armada.conf.sample @@ -0,0 +1,441 @@ +[DEFAULT] + +# +# From armada.conf +# + +# IDs of approved API access roles. (list value) +#armada_apply_roles = admin + +# The default Keystone authentication url. (string value) +#auth_url = http://0.0.0.0/v3 + +# Path to Kubernetes configurations. (string value) +#kubernetes_config_path = /home/user/.kube/ + +# Enables or disables Keystone authentication middleware. (boolean value) +#middleware = true + +# The Keystone project domain name used for authentication. (string value) +#project_domain_name = default + +# The Keystone project name used for authentication. (string value) +#project_name = admin + +# Path to SSH private key. (string value) +#ssh_key_path = /home/user/.ssh/ + +# IDs of approved API access roles. (list value) +#tiller_release_roles = admin + +# IDs of approved API access roles. (list value) +#tiller_status_roles = admin + +# +# From oslo.log +# + +# If set to true, the logging level will be set to DEBUG instead of the default +# INFO level. (boolean value) +# Note: This option can be changed without restarting. +#debug = false + +# The name of a logging configuration file. This file is appended to any +# existing logging configuration files. For details about logging configuration +# files, see the Python logging module documentation. Note that when logging +# configuration files are used then all logging configuration is set in the +# configuration file and other logging configuration options are ignored (for +# example, logging_context_format_string). (string value) +# Note: This option can be changed without restarting. +# Deprecated group/name - [DEFAULT]/log_config +#log_config_append = + +# Defines the format string for %%(asctime)s in log records. Default: +# %(default)s . This option is ignored if log_config_append is set. (string +# value) +#log_date_format = %Y-%m-%d %H:%M:%S + +# (Optional) Name of log file to send logging output to. If no default is set, +# logging will go to stderr as defined by use_stderr. This option is ignored if +# log_config_append is set. (string value) +# Deprecated group/name - [DEFAULT]/logfile +#log_file = + +# (Optional) The base directory used for relative log_file paths. This option +# is ignored if log_config_append is set. (string value) +# Deprecated group/name - [DEFAULT]/logdir +#log_dir = + +# Uses logging handler designed to watch file system. When log file is moved or +# removed this handler will open a new log file with specified path +# instantaneously. It makes sense only if log_file option is specified and +# Linux platform is used. This option is ignored if log_config_append is set. +# (boolean value) +#watch_log_file = false + +# Use syslog for logging. Existing syslog format is DEPRECATED and will be +# changed later to honor RFC5424. This option is ignored if log_config_append +# is set. (boolean value) +#use_syslog = false + +# Enable journald for logging. If running in a systemd environment you may wish +# to enable journal support. Doing so will use the journal native protocol +# which includes structured metadata in addition to log messages.This option is +# ignored if log_config_append is set. (boolean value) +#use_journal = false + +# Syslog facility to receive log lines. This option is ignored if +# log_config_append is set. (string value) +#syslog_log_facility = LOG_USER + +# Log output to standard error. This option is ignored if log_config_append is +# set. (boolean value) +#use_stderr = false + +# Format string to use for log messages with context. (string value) +#logging_context_format_string = %(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [%(request_id)s %(user_identity)s] %(instance)s%(message)s + +# Format string to use for log messages when context is undefined. (string +# value) +#logging_default_format_string = %(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [-] %(instance)s%(message)s + +# Additional data to append to log message when logging level for the message +# is DEBUG. (string value) +#logging_debug_format_suffix = %(funcName)s %(pathname)s:%(lineno)d + +# Prefix each line of exception output with this format. (string value) +#logging_exception_prefix = %(asctime)s.%(msecs)03d %(process)d ERROR %(name)s %(instance)s + +# Defines the format string for %(user_identity)s that is used in +# logging_context_format_string. (string value) +#logging_user_identity_format = %(user)s %(tenant)s %(domain)s %(user_domain)s %(project_domain)s + +# List of package logging levels in logger=LEVEL pairs. This option is ignored +# if log_config_append is set. (list value) +#default_log_levels = amqp=WARN,amqplib=WARN,boto=WARN,qpid=WARN,sqlalchemy=WARN,suds=INFO,oslo.messaging=INFO,oslo_messaging=INFO,iso8601=WARN,requests.packages.urllib3.connectionpool=WARN,urllib3.connectionpool=WARN,websocket=WARN,requests.packages.urllib3.util.retry=WARN,urllib3.util.retry=WARN,keystonemiddleware=WARN,routes.middleware=WARN,stevedore=WARN,taskflow=WARN,keystoneauth=WARN,oslo.cache=INFO,dogpile.core.dogpile=INFO + +# Enables or disables publication of error events. (boolean value) +#publish_errors = false + +# The format for an instance that is passed with the log message. (string +# value) +#instance_format = "[instance: %(uuid)s] " + +# The format for an instance UUID that is passed with the log message. (string +# value) +#instance_uuid_format = "[instance: %(uuid)s] " + +# Interval, number of seconds, of log rate limiting. (integer value) +#rate_limit_interval = 0 + +# Maximum number of logged messages per rate_limit_interval. (integer value) +#rate_limit_burst = 0 + +# Log level name used by rate limiting: CRITICAL, ERROR, INFO, WARNING, DEBUG +# or empty string. Logs with level greater or equal to rate_limit_except_level +# are not filtered. An empty string means that all levels are filtered. (string +# value) +#rate_limit_except_level = CRITICAL + +# Enables or disables fatal status of deprecations. (boolean value) +#fatal_deprecations = false + + +[cors] + +# +# From oslo.middleware +# + +# Indicate whether this resource may be shared with the domain received in the +# requests "origin" header. Format: "://[:]", no trailing +# slash. Example: https://horizon.example.com (list value) +#allowed_origin = + +# Indicate that the actual request can include user credentials (boolean value) +#allow_credentials = true + +# Indicate which headers are safe to expose to the API. Defaults to HTTP Simple +# Headers. (list value) +#expose_headers = + +# Maximum cache age of CORS preflight requests. (integer value) +#max_age = 3600 + +# Indicate which methods can be used during the actual request. (list value) +#allow_methods = OPTIONS,GET,HEAD,POST,PUT,DELETE,TRACE,PATCH + +# Indicate which header field names may be used during the actual request. +# (list value) +#allow_headers = + + +[healthcheck] + +# +# From oslo.middleware +# + +# DEPRECATED: The path to respond to healtcheck requests on. (string value) +# This option is deprecated for removal. +# Its value may be silently ignored in the future. +#path = /healthcheck + +# Show more detailed information as part of the response (boolean value) +#detailed = false + +# Additional backends that can perform health checks and report that +# information back as part of a request. (list value) +#backends = + +# Check the presence of a file to determine if an application is running on a +# port. Used by DisableByFileHealthcheck plugin. (string value) +#disable_by_file_path = + +# Check the presence of a file based on a port to determine if an application +# is running on a port. Expects a "port:path" list of strings. Used by +# DisableByFilesPortsHealthcheck plugin. (list value) +#disable_by_file_paths = + + +[keystone_authtoken] + +# +# From armada.conf +# + +# Authentication URL (string value) +#auth_url = + +# Domain ID to scope to (string value) +#domain_id = + +# Domain name to scope to (string value) +#domain_name = + +# Project ID to scope to (string value) +# Deprecated group/name - [keystone_authtoken]/tenant_id +#project_id = + +# Project name to scope to (string value) +# Deprecated group/name - [keystone_authtoken]/tenant_name +#project_name = + +# Domain ID containing project (string value) +#project_domain_id = + +# Domain name containing project (string value) +#project_domain_name = + +# Trust ID (string value) +#trust_id = + +# Optional domain ID to use with v3 and v2 parameters. It will be used for both +# the user and project domain in v3 and ignored in v2 authentication. (string +# value) +#default_domain_id = + +# Optional domain name to use with v3 API and v2 parameters. It will be used +# for both the user and project domain in v3 and ignored in v2 authentication. +# (string value) +#default_domain_name = + +# User id (string value) +#user_id = + +# Username (string value) +# Deprecated group/name - [keystone_authtoken]/user_name +#username = + +# User's domain id (string value) +#user_domain_id = + +# User's domain name (string value) +#user_domain_name = + +# User's password (string value) +#password = + +# +# From keystonemiddleware.auth_token +# + +# Complete "public" Identity API endpoint. This endpoint should not be an +# "admin" endpoint, as it should be accessible by all end users. +# Unauthenticated clients are redirected to this endpoint to authenticate. +# Although this endpoint should ideally be unversioned, client support in the +# wild varies. If you're using a versioned v2 endpoint here, then this should +# *not* be the same endpoint the service user utilizes for validating tokens, +# because normal end users may not be able to reach that endpoint. (string +# value) +#auth_uri = + +# API version of the admin Identity API endpoint. (string value) +#auth_version = + +# Do not handle authorization requests within the middleware, but delegate the +# authorization decision to downstream WSGI components. (boolean value) +#delay_auth_decision = false + +# Request timeout value for communicating with Identity API server. (integer +# value) +#http_connect_timeout = + +# How many times are we trying to reconnect when communicating with Identity +# API Server. (integer value) +#http_request_max_retries = 3 + +# Request environment key where the Swift cache object is stored. When +# auth_token middleware is deployed with a Swift cache, use this option to have +# the middleware share a caching backend with swift. Otherwise, use the +# ``memcached_servers`` option instead. (string value) +#cache = + +# Required if identity server requires client certificate (string value) +#certfile = + +# Required if identity server requires client certificate (string value) +#keyfile = + +# A PEM encoded Certificate Authority to use when verifying HTTPs connections. +# Defaults to system CAs. (string value) +#cafile = + +# Verify HTTPS connections. (boolean value) +#insecure = false + +# The region in which the identity server can be found. (string value) +#region_name = + +# Directory used to cache files related to PKI tokens. (string value) +#signing_dir = + +# Optionally specify a list of memcached server(s) to use for caching. If left +# undefined, tokens will instead be cached in-process. (list value) +# Deprecated group/name - [keystone_authtoken]/memcache_servers +#memcached_servers = + +# In order to prevent excessive effort spent validating tokens, the middleware +# caches previously-seen tokens for a configurable duration (in seconds). Set +# to -1 to disable caching completely. (integer value) +#token_cache_time = 300 + +# Determines the frequency at which the list of revoked tokens is retrieved +# from the Identity service (in seconds). A high number of revocation events +# combined with a low cache duration may significantly reduce performance. Only +# valid for PKI tokens. (integer value) +#revocation_cache_time = 10 + +# (Optional) If defined, indicate whether token data should be authenticated or +# authenticated and encrypted. If MAC, token data is authenticated (with HMAC) +# in the cache. If ENCRYPT, token data is encrypted and authenticated in the +# cache. If the value is not one of these options or empty, auth_token will +# raise an exception on initialization. (string value) +# Allowed values: None, MAC, ENCRYPT +#memcache_security_strategy = None + +# (Optional, mandatory if memcache_security_strategy is defined) This string is +# used for key derivation. (string value) +#memcache_secret_key = + +# (Optional) Number of seconds memcached server is considered dead before it is +# tried again. (integer value) +#memcache_pool_dead_retry = 300 + +# (Optional) Maximum total number of open connections to every memcached +# server. (integer value) +#memcache_pool_maxsize = 10 + +# (Optional) Socket timeout in seconds for communicating with a memcached +# server. (integer value) +#memcache_pool_socket_timeout = 3 + +# (Optional) Number of seconds a connection to memcached is held unused in the +# pool before it is closed. (integer value) +#memcache_pool_unused_timeout = 60 + +# (Optional) Number of seconds that an operation will wait to get a memcached +# client connection from the pool. (integer value) +#memcache_pool_conn_get_timeout = 10 + +# (Optional) Use the advanced (eventlet safe) memcached client pool. The +# advanced pool will only work under python 2.x. (boolean value) +#memcache_use_advanced_pool = false + +# (Optional) Indicate whether to set the X-Service-Catalog header. If False, +# middleware will not ask for service catalog on token validation and will not +# set the X-Service-Catalog header. (boolean value) +#include_service_catalog = true + +# Used to control the use and type of token binding. Can be set to: "disabled" +# to not check token binding. "permissive" (default) to validate binding +# information if the bind type is of a form known to the server and ignore it +# if not. "strict" like "permissive" but if the bind type is unknown the token +# will be rejected. "required" any form of token binding is needed to be +# allowed. Finally the name of a binding method that must be present in tokens. +# (string value) +#enforce_token_bind = permissive + +# If true, the revocation list will be checked for cached tokens. This requires +# that PKI tokens are configured on the identity server. (boolean value) +#check_revocations_for_cached = false + +# Hash algorithms to use for hashing PKI tokens. This may be a single algorithm +# or multiple. The algorithms are those supported by Python standard +# hashlib.new(). The hashes will be tried in the order given, so put the +# preferred one first for performance. The result of the first hash will be +# stored in the cache. This will typically be set to multiple values only while +# migrating from a less secure algorithm to a more secure one. Once all the old +# tokens are expired this option should be set to a single value for better +# performance. (list value) +#hash_algorithms = md5 + +# Authentication type to load (string value) +# Deprecated group/name - [keystone_authtoken]/auth_plugin +#auth_type = + +# Config Section from which to load plugin specific options (string value) +#auth_section = + + +[oslo_middleware] + +# +# From oslo.middleware +# + +# The maximum body size for each request, in bytes. (integer value) +# Deprecated group/name - [DEFAULT]/osapi_max_request_body_size +# Deprecated group/name - [DEFAULT]/max_request_body_size +#max_request_body_size = 114688 + +# DEPRECATED: The HTTP Header that will be used to determine what the original +# request protocol scheme was, even if it was hidden by a SSL termination +# proxy. (string value) +# This option is deprecated for removal. +# Its value may be silently ignored in the future. +#secure_proxy_ssl_header = X-Forwarded-Proto + +# Whether the application is behind a proxy or not. This determines if the +# middleware should parse the headers or not. (boolean value) +#enable_proxy_headers_parsing = false + + +[oslo_policy] + +# +# From oslo.policy +# + +# The file that defines policies. (string value) +#policy_file = policy.json + +# Default rule. Enforced when a requested rule is not found. (string value) +#policy_default_rule = default + +# Directories where policy configuration files are stored. They can be relative +# to any directory in the search path defined by the config_dir option, or +# absolute paths. The file defined by policy_file must exist for these +# directories to be searched. Missing or empty directories are ignored. (multi +# valued) +#policy_dirs = policy.d diff --git a/etc/armada/policy.yaml b/etc/armada/policy.yaml new file mode 100644 index 00000000..4b09078a --- /dev/null +++ b/etc/armada/policy.yaml @@ -0,0 +1,33 @@ +# +#"admin_required": "role:admin" + +# +#"service_or_admin": "rule:admin_required or rule:service_role" + +# +#"service_role": "role:service" + +# install manifest charts +# POST api/v1.0/apply/ +#"armada:create_endpoints": "rule:admin_required" + +# validate installed manifest +# POST /api/v1.0/validate/ +#"armada:validate_manifest": "rule:admin_required" + +# validate install manifest +# GET /api/v1.0/test/{release} +#"armada:test_release": "rule:admin_required" + +# validate install manifest +# POST /api/v1.0/tests/ +#"armada:test_manifest": "rule:admin_required" + +# Get tiller status +# GET /api/v1.0/status/ +#"tiller:get_status": "rule:admin_required" + +# Get tiller release +# GET /api/v1.0/releases/ +#"tiller:get_release": "rule:admin_required" + diff --git a/examples/armada-keystone-manifest.yaml b/examples/armada-keystone-manifest.yaml new file mode 100644 index 00000000..d941434c --- /dev/null +++ b/examples/armada-keystone-manifest.yaml @@ -0,0 +1,13 @@ +--- +schema: armada/Chart/v1 +metadata: + schema: metadata/Document/v1 + name: keystone +data: + values: + bootstrap: + script: | + openstack domain create 'ucp' + openstack project create --domain 'ucp' 'service' + openstack user create --domain ucp --project service --project-domain 'ucp' --password armada armada + openstack role add --project-domain ucp --user-domain ucp --user armada --project service admin diff --git a/examples/keystone-manifest.yaml b/examples/keystone-manifest.yaml index ee1a21e4..38309ea2 100644 --- a/examples/keystone-manifest.yaml +++ b/examples/keystone-manifest.yaml @@ -10,7 +10,7 @@ data: values: {} source: type: git - location: git://github.com/openstack/openstack-helm + location: https://git.openstack.org/openstack/openstack-helm subpath: helm-toolkit reference: master dependencies: [] @@ -34,7 +34,7 @@ data: values: {} source: type: git - location: git://github.com/openstack/openstack-helm + location: https://git.openstack.org/openstack/openstack-helm subpath: mariadb reference: master dependencies: @@ -59,7 +59,7 @@ data: values: {} source: type: git - location: git://github.com/openstack/openstack-helm + location: https://git.openstack.org/openstack/openstack-helm subpath: memcached reference: master dependencies: @@ -82,11 +82,18 @@ data: no_hooks: false upgrade: no_hooks: false + pre: + delete: + - name: keystone-bootstrap + type: job + labels: + - application: keystone + - component: bootstrap values: - replicas: 2 + replicas: 3 source: type: git - location: git://github.com/openstack/openstack-helm + location: https://git.openstack.org/openstack/openstack-helm subpath: keystone reference: master dependencies: diff --git a/requirements.txt b/requirements.txt index e4f56834..b1976d8a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -gitpython==2.1.5 +gitpython grpcio==1.6.0rc1 grpcio-tools==1.6.0rc1 keystoneauth1==2.21.0 @@ -6,18 +6,17 @@ keystonemiddleware==4.9.1 kubernetes>=1.0.0 protobuf>=3.4.0 PyYAML==3.12 -requests==2.17.3 +requests supermutes==0.2.5 -urllib3==1.21.1 Paste>=2.0.3 PasteDeploy>=1.5.2 # API -falcon==1.1.0 +falcon uwsgi>=2.0.15 # CLI -cliff==2.7.0 +click>=6.7 # Oslo oslo.cache>=1.5.0 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg index 653ebfe3..b7057887 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,11 +40,6 @@ upload-dir = doc/build/html [entry_points] console_scripts = armada = armada.shell:main -armada = - apply = armada.cli.apply:ApplyChartsCommand - tiller = armada.cli.tiller:TillerServerCommand - validate = armada.cli.validate:ValidateYamlCommand - test = armada.cli.test:TestServerCommand oslo.config.opts = armada.conf = armada.conf.opts:list_opts oslo.policy.policies = diff --git a/tools/keystone-account.sh b/tools/keystone-account.sh index 0bca080a..7a24fc18 100755 --- a/tools/keystone-account.sh +++ b/tools/keystone-account.sh @@ -1,5 +1,9 @@ #!/usr/bin/env bash +if [ -x $(which openstack) ]; then + pip install python-openstackclient +fi + openstack domain create 'ucp' openstack project create --domain 'ucp' 'service' openstack user create --domain ucp --project service --project-domain 'ucp' --password armada armada