[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
|
from armada.conf import default
|
||||||
|
|
||||||
import falcon
|
|
||||||
import json
|
import json
|
||||||
from falcon import HTTP_200
|
from falcon import HTTP_200
|
||||||
|
|
||||||
|
@ -26,7 +25,6 @@ from oslo_log import log as logging
|
||||||
default.register_opts()
|
default.register_opts()
|
||||||
|
|
||||||
from armada.handlers.tiller import Tiller as tillerHandler
|
from armada.handlers.tiller import Tiller as tillerHandler
|
||||||
from armada.handlers.armada import Armada as armadaHandler
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
|
@ -34,11 +32,7 @@ DOMAIN = "armada"
|
||||||
|
|
||||||
logging.setup(CONF, DOMAIN)
|
logging.setup(CONF, DOMAIN)
|
||||||
|
|
||||||
class Tiller(object):
|
class Status(object):
|
||||||
'''
|
|
||||||
tiller service endpoints
|
|
||||||
'''
|
|
||||||
|
|
||||||
def on_get(self, req, resp):
|
def on_get(self, req, resp):
|
||||||
'''
|
'''
|
||||||
get tiller status
|
get tiller status
|
||||||
|
@ -54,28 +48,18 @@ class Tiller(object):
|
||||||
resp.content_type = 'application/json'
|
resp.content_type = 'application/json'
|
||||||
resp.status = HTTP_200
|
resp.status = HTTP_200
|
||||||
|
|
||||||
class Armada(object):
|
class Release(object):
|
||||||
'''
|
def on_get(self, req, resp):
|
||||||
apply armada endpoint service
|
'''
|
||||||
'''
|
get tiller releases
|
||||||
|
'''
|
||||||
|
# Get tiller releases
|
||||||
|
handler = tillerHandler()
|
||||||
|
|
||||||
def on_post(self, req, resp):
|
releases = {}
|
||||||
armada = armadaHandler(req.stream.read())
|
for release in handler.list_releases():
|
||||||
armada.sync()
|
releases[release.name] = release.namespace
|
||||||
|
|
||||||
resp.data = json.dumps({'message': 'Success'})
|
resp.data = json.dumps({'releases': releases})
|
||||||
resp.content_type = 'application/json'
|
resp.content_type = 'application/json'
|
||||||
resp.status = HTTP_200
|
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
|
from oslo_config import cfg
|
||||||
|
|
||||||
default_options = [
|
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(
|
cfg.BoolOpt(
|
||||||
'debug',
|
'debug',
|
||||||
default='false',
|
default='false',
|
||||||
|
@ -105,6 +116,21 @@ default_options = [
|
||||||
help='Defines the format string for \
|
help='Defines the format string for \
|
||||||
%(user_identity)s that is used in logging_context_format_string.'),
|
%(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(
|
cfg.BoolOpt(
|
||||||
'publish_errors',
|
'publish_errors',
|
||||||
default='true',
|
default='true',
|
||||||
|
@ -152,6 +178,16 @@ default_options = [
|
||||||
default='true',
|
default='true',
|
||||||
help='Log output to syslog.'),
|
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(
|
cfg.BoolOpt(
|
||||||
'watch_log_file',
|
'watch_log_file',
|
||||||
default='false',
|
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
|
grpc==0.3.post19
|
||||||
grpcio==1.1.3
|
grpcio==1.1.3
|
||||||
grpcio-tools==1.1.3
|
grpcio-tools==1.1.3
|
||||||
|
keystoneauth1==2.21.0
|
||||||
kubernetes>=1.0.0
|
kubernetes>=1.0.0
|
||||||
oslo.log==3.28.0
|
oslo.log==3.28.0
|
||||||
oslo.messaging==5.28.0
|
oslo.messaging==5.28.0
|
||||||
|
|
Loading…
Reference in New Issue