[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:
parent
08f56b9392
commit
05818f6d00
|
@ -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
|
|
@ -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)
|
|
@ -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()
|
|
@ -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)
|
|
@ -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',
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue