[Feat] Add API support for core features

- Refactor API structure
- Add API support for existing CLI flags
- Add Keystone token and RBAC authentication
- Add API documentation
- Add API unit tests
This commit is contained in:
drewwalters96 2017-06-26 10:39:52 -05:00 committed by Alexis Rivera DeLa Torre
parent 08f56b9392
commit 05818f6d00
8 changed files with 412 additions and 28 deletions

View File

@ -0,0 +1,47 @@
# 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.
#
from armada.handlers.armada import Armada as Handler
from falcon import HTTP_200
import json
class Apply(object):
'''
apply armada endpoint service
'''
def on_post(self, req, resp):
# Load data from request and get options
data = json.load(req.stream)
opts = data['options']
# Encode filename
data['file'] = data['file'].encode('utf-8')
armada = Handler(open('../../' + data['file']),
disable_update_pre=opts['disable_update_pre'],
disable_update_post=opts['disable_update_post'],
enable_chart_cleanup=opts['enable_chart_cleanup'],
dry_run=opts['dry_run'],
wait=opts['wait'],
timeout=opts['timeout'])
armada.sync()
resp.data = json.dumps({'message': 'Success'})
resp.content_type = 'application/json'
resp.status = HTTP_200

107
armada/api/middleware.py Normal file
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.
from armada.conf import default
# Required Oslo configuration setup
default.register_opts()
from keystoneauth1 import session
from keystoneauth1.identity import v3
from oslo_config import cfg
from oslo_log import log as logging
import falcon
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
DOMAIN = "armada"
logging.setup(CONF, DOMAIN)
class AuthMiddleware(object):
def process_request(self, req, resp):
# Validate token and get user session
token = req.get_header('X-Auth-Token')
req.context['session'] = self._get_user_session(token)
# Add token roles to request context
req.context['roles'] = self._get_roles(req.context['session'])
def _get_roles(self, session):
# Get roles IDs associated with user
request_url = CONF.auth_url + '/role_assignments'
resp = self._session_request(session=session, request_url=request_url)
json_resp = resp.json()['role_assignments']
role_ids = [r['role']['id'].encode('utf-8') for r in json_resp]
# Get role names associated with role IDs
roles = []
for role_id in role_ids:
request_url = CONF.auth_url + '/roles/' + role_id
resp = self._session_request(session=session,
request_url=request_url)
role = resp.json()['role']['name'].encode('utf-8')
roles.append(role)
return roles
def _get_user_session(self, token):
# Get user session from token
auth = v3.Token(auth_url=CONF.auth_url,
project_name=CONF.project_name,
project_domain_name=CONF.project_domain_name,
token=token)
return session.Session(auth=auth)
def _session_request(self, session, request_url):
try:
return session.get(request_url)
except:
raise falcon.HTTPUnauthorized('Authentication required',
('Authentication token is invalid.'))
class RoleMiddleware(object):
def process_request(self, req, resp):
endpoint = req.path
roles = req.context['roles']
# Verify roles have sufficient permissions for request endpoint
if not (self._verify_roles(endpoint, roles)):
raise falcon.HTTPUnauthorized('Insufficient permissions',
('Token role insufficient.'))
def _verify_roles(self, endpoint, roles):
# Compare the verified roles listed in the config with the user's
# associated roles
if endpoint == '/armada/apply':
approved_roles = CONF.armada_apply_roles
elif endpoint == '/tiller/releases':
approved_roles = CONF.tiller_release_roles
elif endpoint == '/tiller/status':
approved_roles = CONF.tiller_status_roles
verified_roles = set(roles).intersection(approved_roles)
return bool(verified_roles)

57
armada/api/server.py Normal file
View File

@ -0,0 +1,57 @@
# 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.
#
from armada.conf import default
# Required Oslo configuration setup
default.register_opts()
from armada_controller import Apply
from tiller_controller import Release, Status
from middleware import AuthMiddleware, RoleMiddleware
import falcon
from oslo_config import cfg
from oslo_log import log as logging
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
DOMAIN = "armada"
logging.setup(CONF, DOMAIN)
# Build API
def create(middleware=CONF.middleware):
if middleware:
api = falcon.API(middleware=[AuthMiddleware(), RoleMiddleware()])
else:
api = falcon.API()
# Configure API routing
url_routes = (
('/tiller/status', Status()),
('/tiller/releases', Release()),
('/armada/apply/', Apply())
)
for route, service in url_routes:
api.add_route(route, service)
return api
api = create()

View File

@ -15,7 +15,6 @@
from armada.conf import default
import falcon
import json
from falcon import HTTP_200
@ -26,7 +25,6 @@ from oslo_log import log as logging
default.register_opts()
from armada.handlers.tiller import Tiller as tillerHandler
from armada.handlers.armada import Armada as armadaHandler
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
@ -34,11 +32,7 @@ DOMAIN = "armada"
logging.setup(CONF, DOMAIN)
class Tiller(object):
'''
tiller service endpoints
'''
class Status(object):
def on_get(self, req, resp):
'''
get tiller status
@ -54,28 +48,18 @@ class Tiller(object):
resp.content_type = 'application/json'
resp.status = HTTP_200
class Armada(object):
'''
apply armada endpoint service
'''
class Release(object):
def on_get(self, req, resp):
'''
get tiller releases
'''
# Get tiller releases
handler = tillerHandler()
def on_post(self, req, resp):
armada = armadaHandler(req.stream.read())
armada.sync()
releases = {}
for release in handler.list_releases():
releases[release.name] = release.namespace
resp.data = json.dumps({'message': 'Success'})
resp.data = json.dumps({'releases': releases})
resp.content_type = 'application/json'
resp.status = HTTP_200
wsgi_app = api = falcon.API()
# Routing
url_routes = (
('/tiller/status', Tiller()),
('/apply', Armada()),
)
for route, service in url_routes:
api.add_route(route, service)

View File

@ -17,6 +17,17 @@ import os
from oslo_config import cfg
default_options = [
cfg.ListOpt(
'armada_apply_roles',
default=['admin'],
help='IDs of approved API access roles.'),
cfg.StrOpt(
'auth_url',
default='http://0.0.0.0/v3',
help='The default Keystone authentication url.'),
cfg.BoolOpt(
'debug',
default='false',
@ -105,6 +116,21 @@ default_options = [
help='Defines the format string for \
%(user_identity)s that is used in logging_context_format_string.'),
cfg.BoolOpt(
'middleware',
default='true',
help='Enables or disables Keystone authentication middleware.'),
cfg.StrOpt(
'project_domain_name',
default='default',
help='The Keystone project domain name used for authentication.'),
cfg.StrOpt(
'project_name',
default='admin',
help='The Keystone project name used for authentication.'),
cfg.BoolOpt(
'publish_errors',
default='true',
@ -152,6 +178,16 @@ default_options = [
default='true',
help='Log output to syslog.'),
cfg.ListOpt(
'tiller_release_roles',
default=['admin'],
help='IDs of approved API access roles.'),
cfg.ListOpt(
'tiller_status_roles',
default=['admin'],
help='IDs of approved API access roles.'),
cfg.BoolOpt(
'watch_log_file',
default='false',

View File

@ -0,0 +1,76 @@
# 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.
from armada.api import server
from falcon import testing
import json
import mock
class APITestCase(testing.TestCase):
def setUp(self):
super(APITestCase, self).setUp()
self.app = server.create(middleware=False)
class TestAPI(APITestCase):
@mock.patch('armada.api.armada_controller.Handler')
def test_armada_apply(self, mock_armada):
'''
Test /armada/apply endpoint
'''
mock_armada.sync.return_value = None
body = json.dumps({'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': '100'}})
doc = {u'message': u'Success'}
result = self.simulate_post(path='/armada/apply', body=body)
self.assertEqual(result.json, doc)
@mock.patch('armada.api.tiller_controller.tillerHandler')
def test_tiller_status(self, mock_tiller):
'''
Test /tiller/status endpoint
'''
# Mock tiller status value
mock_tiller.tiller_status.return_value = 'Active'
doc = {u'message': u'Tiller Server is Active'}
result = self.simulate_get('/tiller/status')
self.assertEqual(result.json, doc)
@mock.patch('armada.api.tiller_controller.tillerHandler')
def test_tiller_releases(self, mock_tiller):
'''
Test /tiller/releases endpoint
'''
# Mock tiller status value
mock_tiller.list_releases.return_value = None
doc = {u'releases': {}}
result = self.simulate_get('/tiller/releases')
self.assertEqual(result.json, doc)

View File

@ -0,0 +1,76 @@
Armada RESTful API
===================
Armada Endpoints
-----------------
.. http:post:: /armada/apply
: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 boolean dry_run
:>json boolean wait
:>json float timeout
Request:
.. sourcecode:: js
{
"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
}
}
Results:
.. sourcecode:: js
{
"message": "success"
}
Tiller Endpoints
-----------------
.. http:get:: /tiller/releases
Retrieves tiller releases.
Results:
.. sourcecode:: js
{
"releases": {
"armada-memcached": "openstack",
"armada-etcd": "openstack",
"armada-keystone": "openstack",
"armada-rabbitmq": "openstack",
"armada-horizon": "openstack"
}
}
.. http:get:: /tiller/status
Retrieves the status of the Tiller server.
Results:
.. sourcecode:: js
{
"message": Tiller Server is Active
}

View File

@ -1,6 +1,7 @@
grpc==0.3.post19
grpcio==1.1.3
grpcio-tools==1.1.3
keystoneauth1==2.21.0
kubernetes>=1.0.0
oslo.log==3.28.0
oslo.messaging==5.28.0