Browse Source

feat(cli): using-click-framework

- using click framework
- added api client
- allow interactions between code and service endpoints
- documention on the command line
- updated gitignore

Change-Id: Ibe359025f5b35606d876c29fa88e04048f276cc8
changes/20/570020/1
gardlt 2 years ago
parent
commit
7b26e59422
46 changed files with 2195 additions and 488 deletions
  1. +3
    -0
      .dockerignore
  2. +0
    -18
      .editorconfig
  3. +9
    -0
      .gitignore
  4. +2
    -0
      Dockerfile
  5. +28
    -24
      armada/api/__init__.py
  6. +0
    -60
      armada/api/armada_controller.py
  7. +71
    -0
      armada/api/controller/armada.py
  8. +134
    -0
      armada/api/controller/test.py
  9. +23
    -20
      armada/api/controller/tiller.py
  10. +8
    -17
      armada/api/controller/validation.py
  11. +11
    -5
      armada/api/middleware.py
  12. +15
    -17
      armada/api/server.py
  13. +19
    -0
      armada/cli/__init__.py
  14. +188
    -54
      armada/cli/apply.py
  15. +118
    -71
      armada/cli/test.py
  16. +80
    -25
      armada/cli/tiller.py
  17. +53
    -24
      armada/cli/validate.py
  18. +107
    -0
      armada/common/client.py
  19. +12
    -2
      armada/common/policies/service.py
  20. +2
    -4
      armada/common/policies/tiller.py
  21. +96
    -0
      armada/common/session.py
  22. +3
    -2
      armada/conf/__init__.py
  23. +8
    -1
      armada/conf/default.py
  24. +3
    -0
      armada/const.py
  25. +18
    -0
      armada/exceptions/api_exceptions.py
  26. +5
    -12
      armada/handlers/armada.py
  27. +7
    -2
      armada/handlers/k8s.py
  28. +6
    -6
      armada/handlers/tiller.py
  29. +66
    -24
      armada/shell.py
  30. +8
    -5
      armada/tests/unit/api/test_api.py
  31. +0
    -0
      armada/tests/unit/common/test_policy.py
  32. +32
    -5
      docs/source/commands/apply.rst
  33. +20
    -3
      docs/source/commands/test.rst
  34. +17
    -3
      docs/source/commands/tiller.rst
  35. +8
    -3
      docs/source/commands/validate.rst
  36. +5
    -5
      docs/source/development/getting-started.rst
  37. +493
    -48
      docs/source/operations/guide-api.rst
  38. +8
    -7
      docs/source/operations/guide-configure.rst
  39. +2
    -6
      entrypoint.sh
  40. +441
    -0
      etc/armada/armada.conf.sample
  41. +33
    -0
      etc/armada/policy.yaml
  42. +13
    -0
      examples/armada-keystone-manifest.yaml
  43. +12
    -5
      examples/keystone-manifest.yaml
  44. +4
    -5
      requirements.txt
  45. +0
    -5
      setup.cfg
  46. +4
    -0
      tools/keystone-account.sh

+ 3
- 0
.dockerignore View File

@@ -5,3 +5,6 @@ CODE_OF_CONDUCT.rst
ChangeLog
LICENSE
OWNERS
etc/armada/armada.conf
etc/armada/policy.yaml
charts/*

+ 0
- 18
.editorconfig View File

@@ -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


+ 9
- 0
.gitignore View File

@@ -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

+ 2
- 0
Dockerfile View File

@@ -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



+ 28
- 24
armada/api/__init__.py View File

@@ -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)

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")
raw_body = req.stream.read(req.content_length or 0)

if raw_body is None:
return None

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({


+ 0
- 60
armada/api/armada_controller.py View File

@@ -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))

+ 71
- 0
armada/api/controller/armada.py View File

@@ -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)

+ 134
- 0
armada/api/controller/test.py View File

@@ -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)

armada/api/tiller_controller.py → armada/api/controller/tiller.py View File

@@ -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)

armada/api/validation_controller.py → armada/api/controller/validation.py View File

@@ -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)

+ 11
- 5
armada/api/middleware.py View File

@@ -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)

+ 15
- 17
armada/api/server.py View File

@@ -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



+ 19
- 0
armada/cli/__init__.py View File

@@ -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()

+ 188
- 54
armada/cli/apply.py View File

@@ -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

CONF = cfg.CONF


@click.group()
def apply():
""" Apply manifest to cluster

"""


DESC = """
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:

\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')

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()


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')

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

def take_action(self, parsed_args):
applyCharts(parsed_args)
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'))

+ 118
- 71
armada/cli/test.py View File

@@ -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):

tiller = Tiller(tiller_host=args.tiller_host, tiller_port=args.tiller_port)
known_release_names = [release[0] for release in tiller.list_charts()]
DESC = """
This command test deployed charts

if args.release:
LOG.info("RUNNING: %s tests", args.release)
resp = tiller.testing_release(args.release)
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 not resp:
LOG.info("FAILED: %s", args.release)
return
To test armada deployed releases:

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)
$ armada test --file examples/simple.yaml

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)
To test release:

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'))
$ armada test --release blog-1

if release_name in known_release_names:
LOG.info('RUNNING: %s tests', release_name)
resp = tiller.testing_release(release_name)
"""

if not resp:
continue
SHORT_DESC = "command test releases"

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)


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')

return parser

def take_action(self, parsed_args):
testService(parsed_args)
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'))

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)

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:
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)

+ 80
- 25
armada/cli/tiller.py View File

@@ -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

"""


DESC = """
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

"""

SHORT_DESC = "command gets tiller infromation"


CONF = cfg.CONF
@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 tillerServer(args):
class TillerServices(CliAction):

tiller = Tiller()
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

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))
def invoke(self):

if args.releases:
for release in tiller.list_releases():
LOG.info("Release: %s ( namespace= %s )", release.name,
release.namespace)
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')

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
self.logger.info("Tiller Service: %s", tiller_status)
self.logger.info("Tiller Version: %s", tiller_version)

def take_action(self, parsed_args):
tillerServer(parsed_args)
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)

+ 53
- 24
armada/cli/validate.py View File

@@ -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

"""


DESC = """
This command validates Armada Manifest

The validate argument must be a relative path to Armada manifest

$ armada validate examples/simple.yaml

"""

SHORT_DESC = "command validates Armada Manifest"

CONF = cfg.CONF

@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 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)

try:
if doc_check and obj_check:
LOG.info('Successfully validated: %s', args.file)
except Exception:
raise Exception('Failed to validate: %s', args.file)
class ValidateManifest(CliAction):

def __init__(self, ctx, filename):
super(ValidateManifest, self).__init__()
self.ctx = ctx
self.filename = filename

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
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)

def take_action(self, parsed_args):
validateYaml(parsed_args)
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)

+ 107
- 0
armada/common/client.py View File

@@ -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))

+ 12
- 2
armada/common/policies/service.py View File

@@ -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': '/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': '/v1.0/validate/', 'method': 'POST'}]),
operations=[{'path': '/api/v1.0/tests/', 'method': 'POST'}]),
]




+ 2
- 4
armada/common/policies/tiller.py View File

@@ -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'}]),
]




+ 96
- 0
armada/common/session.py View File

@@ -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

+ 3
- 2
armada/conf/__init__.py View File

@@ -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():


+ 8
- 1
armada/conf/default.py View File

@@ -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')}

+ 3
- 0
armada/const.py View File

@@ -28,3 +28,6 @@ KEYWORD_CHART = 'chart'
# Statuses
STATUS_DEPLOYED = 'DEPLOYED'
STATUS_FAILED = 'FAILED'

# Configuration File
CONFIG_PATH = '/etc/armada/armada.conf'

+ 18
- 0
armada/exceptions/api_exceptions.py View File

@@ -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.'

+ 5
- 12
armada/handlers/armada.py View File

@@ -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)


+ 7
- 2
armada/handlers/k8s.py View File

@@ -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()


+ 6
- 6
armada/handlers/tiller.py View File

@@ -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()



+ 66
- 24
armada/shell.py View File

@@ -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:

def main(argv=None):
if argv is None:
argv = sys.argv[1:]
return ArmadaApp().run(argv)
\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')


main.add_command(apply_create)
main.add_command(test_charts)
main.add_command(tiller_service)
main.add_command(validate_manifest)

+ 8
- 5
armada/tests/unit/api/test_api.py View File

@@ -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


armada/tests/unit/test_policy.py → armada/tests/unit/common/test_policy.py View File


+ 32
- 5
docs/source/commands/apply.rst View File

@@ -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

Options:
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/
</