feat(yaml): Support document references
This allows the user to apply and/or validate a manifest using either a filepath (as before) or URL. Addition by sh8121att: - Create a general document resolver class to handle local paths and URIs - Allow multiple filenames and combine them into a single document set - Change API to allow for passing document reference URIs to be resolved server-side rather - Update validation API to conform to UCP specification - Dockerfile updates to speed up build - Fix unit tests Closes #96 Change-Id: I5a57779f10d1b63ffc161a14afec851a34ae9efe
This commit is contained in:
parent
be7b49fb14
commit
d383e772fd
23
Dockerfile
23
Dockerfile
@ -1,4 +1,4 @@
|
||||
FROM ubuntu:16.04
|
||||
FROM python:3.5
|
||||
|
||||
MAINTAINER Armada Team
|
||||
|
||||
@ -6,33 +6,20 @@ ENV DEBIAN_FRONTEND noninteractive
|
||||
ENV LANG=C.UTF-8
|
||||
ENV LC_ALL=C.UTF-8
|
||||
|
||||
COPY . /armada
|
||||
COPY requirements.txt /tmp/
|
||||
RUN pip3 install -r /tmp/requirements.txt
|
||||
|
||||
COPY . /armada
|
||||
RUN apt-get update && \
|
||||
apt-get upgrade -y && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
netbase \
|
||||
python3-pip && \
|
||||
apt-get install -y \
|
||||
build-essential \
|
||||
curl \
|
||||
git \
|
||||
python3-minimal \
|
||||
python3-setuptools \
|
||||
python3-dev && \
|
||||
git && \
|
||||
useradd -u 1000 -g users -d /armada armada && \
|
||||
chown -R armada:users /armada && \
|
||||
mv /armada/etc/armada /etc/ && \
|
||||
\
|
||||
cd /armada && \
|
||||
pip3 install --upgrade pip && \
|
||||
pip3 install -r requirements.txt && \
|
||||
python3 setup.py install && \
|
||||
\
|
||||
apt-get purge --auto-remove -y \
|
||||
build-essential \
|
||||
curl && \
|
||||
apt-get clean -y && \
|
||||
rm -rf \
|
||||
/root/.cache \
|
||||
/var/lib/apt/lists/*
|
||||
|
@ -59,6 +59,24 @@ class BaseResource(object):
|
||||
raise Exception(
|
||||
"%s: Invalid YAML in body: %s" % (req.path, jex))
|
||||
|
||||
def req_json(self, req):
|
||||
if req.content_length is None or req.content_length == 0:
|
||||
return None
|
||||
|
||||
raw_body = req.stream.read(req.content_length or 0)
|
||||
|
||||
if raw_body is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
return json.loads(raw_body.decode())
|
||||
except json.JSONDecodeError as jex:
|
||||
self.error(
|
||||
req.context,
|
||||
"Invalid JSON in request: %s" % str(jex))
|
||||
raise Exception(
|
||||
"%s: Invalid JSON in body: %s" % (req.path, jex))
|
||||
|
||||
def return_error(self, resp, status_code, message="", retry=False):
|
||||
resp.body = json.dumps({
|
||||
'type': 'error',
|
||||
|
@ -13,12 +13,15 @@
|
||||
# limitations under the License.
|
||||
|
||||
import json
|
||||
import yaml
|
||||
|
||||
import falcon
|
||||
|
||||
from armada import api
|
||||
from armada.common import policy
|
||||
from armada.handlers.armada import Armada
|
||||
from armada.handlers.document import ReferenceResolver
|
||||
from armada.handlers.override import Override
|
||||
|
||||
|
||||
class Apply(api.BaseResource):
|
||||
@ -30,17 +33,45 @@ class Apply(api.BaseResource):
|
||||
try:
|
||||
|
||||
# Load data from request and get options
|
||||
if req.content_type == 'application/x-yaml':
|
||||
data = list(self.req_yaml(req))
|
||||
if type(data[0]) is list:
|
||||
documents = list(data[0])
|
||||
else:
|
||||
documents = data
|
||||
elif req.content_type == 'application/json':
|
||||
self.logger.debug("Applying manifest based on reference.")
|
||||
req_body = self.req_json(req)
|
||||
doc_ref = req_body.get('hrefs', None)
|
||||
|
||||
data = list(self.req_yaml(req))
|
||||
if not doc_ref:
|
||||
self.logger.info("Request did not contain 'hrefs'.")
|
||||
resp.status = falcon.HTTP_400
|
||||
return
|
||||
|
||||
if type(data[0]) is list:
|
||||
data = list(data[0])
|
||||
data = ReferenceResolver.resolve_reference(doc_ref)
|
||||
documents = list()
|
||||
for d in data:
|
||||
documents.extend(list(yaml.safe_load_all(d.decode())))
|
||||
|
||||
if req_body.get('overrides', None):
|
||||
overrides = Override(documents,
|
||||
overrides=req_body.get('overrides'))
|
||||
documents = overrides.update_manifests()
|
||||
else:
|
||||
self.error(req.context, "Unknown content-type %s"
|
||||
% req.content_type)
|
||||
self.return_error(
|
||||
resp,
|
||||
falcon.HTTP_415,
|
||||
message="Request must be in application/x-yaml"
|
||||
"or application/json")
|
||||
|
||||
opts = req.params
|
||||
|
||||
# Encode filename
|
||||
armada = Armada(
|
||||
data,
|
||||
documents,
|
||||
disable_update_pre=req.get_param_as_bool(
|
||||
'disable_update_pre'),
|
||||
disable_update_post=req.get_param_as_bool(
|
||||
|
@ -13,12 +13,13 @@
|
||||
# limitations under the License.
|
||||
|
||||
import json
|
||||
|
||||
import falcon
|
||||
import yaml
|
||||
|
||||
from armada import api
|
||||
from armada.common import policy
|
||||
from armada.utils.lint import validate_armada_documents
|
||||
from armada.handlers.document import ReferenceResolver
|
||||
|
||||
|
||||
class Validate(api.BaseResource):
|
||||
@ -29,19 +30,58 @@ class Validate(api.BaseResource):
|
||||
@policy.enforce('armada:validate_manifest')
|
||||
def on_post(self, req, resp):
|
||||
try:
|
||||
manifest = self.req_yaml(req)
|
||||
documents = list(manifest)
|
||||
if req.content_type == 'application/json':
|
||||
self.logger.debug("Validating manifest based on reference.")
|
||||
json_body = self.req_json(req)
|
||||
if json_body.get('href', None):
|
||||
self.logger.debug("Validating manifest from reference %s."
|
||||
% json_body.get('href'))
|
||||
data = ReferenceResolver.resolve_reference(
|
||||
json_body.get('href'))
|
||||
documents = list()
|
||||
for d in data:
|
||||
documents.extend(list(yaml.safe_load_all(d.decode())))
|
||||
else:
|
||||
resp.status = falcon.HTTP_400
|
||||
return
|
||||
else:
|
||||
manifest = self.req_yaml(req)
|
||||
documents = list(manifest)
|
||||
|
||||
message = {
|
||||
'valid': validate_armada_documents(documents)
|
||||
self.logger.debug("Validating set of %d documents."
|
||||
% len(documents))
|
||||
|
||||
result = validate_armada_documents(documents)
|
||||
|
||||
resp.content_type = 'application/json'
|
||||
resp_body = {
|
||||
'kind': 'Status',
|
||||
'apiVersion': 'v1.0',
|
||||
'metadata': {},
|
||||
'reason': 'Validation',
|
||||
'details': {
|
||||
'errorCount': 0,
|
||||
'messageList': []
|
||||
},
|
||||
}
|
||||
|
||||
resp.status = falcon.HTTP_200
|
||||
resp.body = json.dumps(message)
|
||||
resp.content_type = 'application/json'
|
||||
if result:
|
||||
resp.status = falcon.HTTP_200
|
||||
resp_body['status'] = 'Success'
|
||||
resp_body['message'] = 'Armada validations succeeded'
|
||||
resp_body['code'] = 200
|
||||
else:
|
||||
resp.status = falcon.HTTP_400
|
||||
resp_body['status'] = 'Failure'
|
||||
resp_body['message'] = 'Armada validations failed'
|
||||
resp_body['code'] = 400
|
||||
resp_body['details']['errorCount'] = 1
|
||||
resp_body['details']['messageList'].\
|
||||
append(dict(message='Validation failed.', error=True))
|
||||
|
||||
except Exception:
|
||||
resp.body = json.dumps(resp_body)
|
||||
except Exception as ex:
|
||||
err_message = 'Failed to validate Armada Manifest'
|
||||
self.error(req.context, err_message)
|
||||
self.logger.error(err_message, exc_info=ex)
|
||||
self.return_error(
|
||||
resp, falcon.HTTP_400, message=err_message)
|
||||
|
@ -15,6 +15,7 @@
|
||||
import falcon
|
||||
from oslo_config import cfg
|
||||
from oslo_policy import policy
|
||||
from oslo_log import log as logging
|
||||
|
||||
from armada import conf
|
||||
from armada.api import ArmadaRequest
|
||||
@ -52,6 +53,9 @@ def create(enable_middleware=CONF.middleware):
|
||||
else:
|
||||
api = falcon.API(request_type=ArmadaRequest)
|
||||
|
||||
logging.set_defaults(default_log_levels=CONF.default_log_levels)
|
||||
logging.setup(CONF, 'armada')
|
||||
|
||||
# Configure API routing
|
||||
url_routes_v1 = (
|
||||
('health', Health()),
|
||||
@ -61,6 +65,7 @@ def create(enable_middleware=CONF.middleware):
|
||||
('tests', Tests()),
|
||||
('test/{release}', Test()),
|
||||
('validate', Validate()),
|
||||
('validatedesign', Validate()),
|
||||
)
|
||||
|
||||
for route, service in url_routes_v1:
|
||||
|
@ -18,7 +18,9 @@ import click
|
||||
from oslo_config import cfg
|
||||
|
||||
from armada.cli import CliAction
|
||||
from armada.exceptions.source_exceptions import InvalidPathException
|
||||
from armada.handlers.armada import Armada
|
||||
from armada.handlers.document import ReferenceResolver
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
@ -64,7 +66,7 @@ SHORT_DESC = "command install manifest charts"
|
||||
|
||||
|
||||
@apply.command(name='apply', help=DESC, short_help=SHORT_DESC)
|
||||
@click.argument('filename')
|
||||
@click.argument('locations', nargs=-1)
|
||||
@click.option('--api', help="Contacts service endpoint", is_flag=True)
|
||||
@click.option(
|
||||
'--disable-update-post', help="run charts without install", is_flag=True)
|
||||
@ -78,66 +80,35 @@ SHORT_DESC = "command install manifest charts"
|
||||
@click.option(
|
||||
'--tiller-port', help="Tiller host port", type=int, default=44134)
|
||||
@click.option(
|
||||
'--timeout', help="specifies time to wait for charts", type=int,
|
||||
'--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('--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):
|
||||
def apply_create(ctx, locations, 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()
|
||||
ApplyManifest(ctx, locations, 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):
|
||||
def __init__(self, ctx, locations, 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
|
||||
# Filename can also be a URL reference
|
||||
self.locations = locations
|
||||
self.api = api
|
||||
self.disable_update_post = disable_update_post
|
||||
self.disable_update_pre = disable_update_pre
|
||||
@ -153,8 +124,7 @@ class ApplyManifest(CliAction):
|
||||
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)
|
||||
self.logger.info('Did not performed chart %s(s)', result)
|
||||
elif result == 'diff' and not resp[result]:
|
||||
self.logger.info('No Relase changes detected')
|
||||
|
||||
@ -167,25 +137,32 @@ class ApplyManifest(CliAction):
|
||||
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)
|
||||
try:
|
||||
doc_data = ReferenceResolver.resolve_reference(self.locations)
|
||||
documents = list()
|
||||
for d in doc_data:
|
||||
documents.extend(list(yaml.safe_load_all(d.decode())))
|
||||
except InvalidPathException as ex:
|
||||
self.logger.error(str(ex))
|
||||
return
|
||||
except yaml.YAMLError as yex:
|
||||
self.logger.error("Invalid YAML found: %s" % str(yex))
|
||||
return
|
||||
|
||||
resp = armada.sync()
|
||||
self.output(resp)
|
||||
armada = Armada(
|
||||
documents, 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:
|
||||
if len(self.values) > 0:
|
||||
self.logger.error(
|
||||
"Cannot specify local values files when using the API.")
|
||||
return
|
||||
|
||||
query = {
|
||||
'disable_update_post': self.disable_update_post,
|
||||
'disable_update_pre': self.disable_update_pre,
|
||||
@ -199,8 +176,6 @@ class ApplyManifest(CliAction):
|
||||
|
||||
client = self.ctx.obj.get('CLIENT')
|
||||
|
||||
with open(self.filename, 'r') as f:
|
||||
resp = client.post_apply(
|
||||
manifest=f.read(), values=self.values, set=self.set,
|
||||
query=query)
|
||||
self.output(resp.get('message'))
|
||||
resp = client.post_apply(
|
||||
manifest_ref=self.locations, set=self.set, query=query)
|
||||
self.output(resp.get('message'))
|
||||
|
@ -12,7 +12,6 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
import click
|
||||
import yaml
|
||||
|
||||
@ -20,6 +19,7 @@ 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 armada.handlers.document import ReferenceResolver
|
||||
|
||||
|
||||
@click.group()
|
||||
@ -42,38 +42,47 @@ SHORT_DESC = "command validates Armada Manifest"
|
||||
|
||||
|
||||
@validate.command(name='validate', help=DESC, short_help=SHORT_DESC)
|
||||
@click.argument('filename')
|
||||
@click.argument('locations', nargs=-1)
|
||||
@click.pass_context
|
||||
def validate_manifest(ctx, filename):
|
||||
ValidateManifest(ctx, filename).invoke()
|
||||
def validate_manifest(ctx, locations):
|
||||
ValidateManifest(ctx, locations).invoke()
|
||||
|
||||
|
||||
class ValidateManifest(CliAction):
|
||||
|
||||
def __init__(self, ctx, filename):
|
||||
def __init__(self, ctx, locations):
|
||||
super(ValidateManifest, self).__init__()
|
||||
self.ctx = ctx
|
||||
self.filename = filename
|
||||
self.locations = locations
|
||||
|
||||
def invoke(self):
|
||||
if not self.ctx.obj.get('api', False):
|
||||
documents = yaml.safe_load_all(open(self.filename).read())
|
||||
doc_data = ReferenceResolver.resolve_reference(self.locations)
|
||||
documents = list()
|
||||
for d in doc_data:
|
||||
documents.extend(list(yaml.safe_load_all(d.decode())))
|
||||
|
||||
manifest_obj = Manifest(documents).get_manifest()
|
||||
obj_check = validate_armada_object(manifest_obj)
|
||||
doc_check = validate_armada_documents(documents)
|
||||
|
||||
try:
|
||||
if doc_check and obj_check:
|
||||
self.logger.info(
|
||||
'Successfully validated: %s', self.filename)
|
||||
self.logger.info('Successfully validated: %s',
|
||||
self.locations)
|
||||
except Exception:
|
||||
raise Exception('Failed to validate: %s', self.filename)
|
||||
raise Exception('Failed to validate: %s', self.locations)
|
||||
else:
|
||||
if len(self.locations) > 1:
|
||||
self.logger.error(
|
||||
"Cannot specify multiple locations "
|
||||
"when using validate API."
|
||||
)
|
||||
return
|
||||
|
||||
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)
|
||||
resp = client.post_validate(self.locations[0])
|
||||
|
||||
if resp.get('code') == 200:
|
||||
self.logger.info('Successfully validated: %s', self.locations)
|
||||
else:
|
||||
self.logger.error("Failed to validate: %s", self.locations)
|
||||
|
@ -27,7 +27,6 @@ API_VERSION = 'v{}/{}'
|
||||
|
||||
|
||||
class ArmadaClient(object):
|
||||
|
||||
def __init__(self, session):
|
||||
self.session = session
|
||||
|
||||
@ -55,22 +54,72 @@ class ArmadaClient(object):
|
||||
def post_validate(self, manifest=None):
|
||||
|
||||
endpoint = self._set_endpoint('1.0', 'validate')
|
||||
resp = self.session.post(endpoint, body=manifest)
|
||||
# TODO(sh8121att) Look to update the UCP convention to
|
||||
# allow a list of hrefs
|
||||
req_body = {'href': manifest}
|
||||
|
||||
resp = self.session.post(
|
||||
endpoint,
|
||||
data=req_body,
|
||||
headers={
|
||||
'content-type': 'application/json'
|
||||
})
|
||||
|
||||
self._check_response(resp)
|
||||
|
||||
return resp.json()
|
||||
|
||||
def post_apply(self, manifest=None, values=None, set=None, query=None):
|
||||
def post_apply(self,
|
||||
manifest=None,
|
||||
manifest_ref=None,
|
||||
values=None,
|
||||
set=None,
|
||||
query=None):
|
||||
"""Call the Armada API to apply a manifest.
|
||||
|
||||
if values or set:
|
||||
document = list(yaml.safe_load_all(manifest))
|
||||
override = Override(
|
||||
document, overrides=set, values=values).update_manifests()
|
||||
manifest = yaml.dump(override)
|
||||
If manifest is not None, then the request body will be a fully
|
||||
rendered set of YAML documents including overrides and
|
||||
values-files application.
|
||||
|
||||
If manifest is None and manifest_ref is not, then the request
|
||||
body will be a JSON structure providing a list of references
|
||||
to Armada manifest documents and a list of overrides. Local
|
||||
values files are not supported when using the API with references.
|
||||
|
||||
:param manifest: string of YAML formatted Armada manifests
|
||||
:param manifest_ref: valid file paths or URIs referring to Armada
|
||||
manifests
|
||||
:param values: list of local files containing values.yaml overrides
|
||||
:param set: list of single-value overrides
|
||||
:param query: explicit query string parameters
|
||||
"""
|
||||
endpoint = self._set_endpoint('1.0', 'apply')
|
||||
resp = self.session.post(endpoint, body=manifest, query=query)
|
||||
|
||||
if manifest:
|
||||
if values or set:
|
||||
document = list(yaml.safe_load_all(manifest))
|
||||
override = Override(
|
||||
document, overrides=set, values=values).update_manifests()
|
||||
manifest = yaml.dump(override)
|
||||
resp = self.session.post(
|
||||
endpoint,
|
||||
body=manifest,
|
||||
query=query,
|
||||
headers={
|
||||
'content-type': 'application/x-yaml'
|
||||
})
|
||||
elif manifest_ref:
|
||||
req_body = {
|
||||
'hrefs': manifest_ref,
|
||||
'overrides': set or [],
|
||||
}
|
||||
resp = self.session.post(
|
||||
endpoint,
|
||||
data=req_body,
|
||||
query=query,
|
||||
headers={
|
||||
'content-type': 'application/json'
|
||||
})
|
||||
|
||||
self._check_response(resp)
|
||||
|
||||
@ -100,8 +149,7 @@ class ArmadaClient(object):
|
||||
"Unauthorized access to %s, include valid token.".format(
|
||||
resp.url))
|
||||
elif resp.status_code == 403:
|
||||
raise err.ClientForbiddenError(
|
||||
"Forbidden access to %s" % resp.url)
|
||||
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))
|
||||
raise err.ClientError("Error - received %d: %s" %
|
||||
(resp.status_code, resp.text))
|
||||
|
@ -55,22 +55,23 @@ class ArmadaSession(object):
|
||||
self.logger = LOG
|
||||
|
||||
# TODO Add keystone authentication to produce a token for this session
|
||||
def get(self, endpoint, query=None):
|
||||
def get(self, endpoint, query=None, headers=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
|
||||
:param headers: Dictionary of HTTP headers to include in request
|
||||
|
||||
:return: A requests.Response object
|
||||
"""
|
||||
api_url = '{}{}'.format(self.base_url, endpoint)
|
||||
resp = self._session.get(
|
||||
api_url, params=query, timeout=3600)
|
||||
api_url, params=query, headers=headers, timeout=3600)
|
||||
|
||||
return resp
|
||||
|
||||
def post(self, endpoint, query=None, body=None, data=None):
|
||||
def post(self, endpoint, query=None, body=None, data=None, headers=None):
|
||||
"""
|
||||
Send a POST request to armada. If both body and data are specified,
|
||||
body will will be used.
|
||||
@ -79,6 +80,7 @@ class ArmadaSession(object):
|
||||
: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.
|
||||
:param headers: Dictionary of HTTP headers to include in request
|
||||
:return: A requests.Response object
|
||||
"""
|
||||
api_url = '{}{}'.format(self.base_url, endpoint)
|
||||
@ -86,11 +88,17 @@ class ArmadaSession(object):
|
||||
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)
|
||||
resp = self._session.post(api_url,
|
||||
params=query,
|
||||
data=body,
|
||||
headers=headers,
|
||||
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)
|
||||
resp = self._session.post(api_url,
|
||||
params=query,
|
||||
json=data,
|
||||
headers=headers,
|
||||
timeout=3600)
|
||||
|
||||
return resp
|
||||
|
@ -24,6 +24,9 @@ CONF = cfg.CONF
|
||||
|
||||
CONFIG_FILES = ['api-paste.ini', 'armada.conf']
|
||||
|
||||
# Load oslo_log options prior to file/CLI parsing
|
||||
log.register_options(CONF)
|
||||
|
||||
|
||||
def _get_config_files(env=None):
|
||||
if env is None:
|
||||
|
140
armada/handlers/document.py
Normal file
140
armada/handlers/document.py
Normal file
@ -0,0 +1,140 @@
|
||||
# Copyright 2017 AT&T Intellectual Property. All other rights reserved.
|
||||
#
|
||||
# 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.
|
||||
"""Module for resolving design references."""
|
||||
|
||||
import urllib.parse
|
||||
import re
|
||||
import requests
|
||||
|
||||
from oslo_log import log as logging
|
||||
|
||||
from armada.exceptions.source_exceptions import InvalidPathException
|
||||
from armada.utils.keystone import KeystoneUtils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ReferenceResolver(object):
|
||||
"""Class for handling different data references to resolve them data."""
|
||||
|
||||
@classmethod
|
||||
def resolve_reference(cls, design_ref):
|
||||
"""Resolve a reference to a design document.
|
||||
|
||||
Locate a schema handler based on the URI scheme of the data reference
|
||||
and use that handler to get the data referenced.
|
||||
|
||||
:param design_ref: A list of URI-formatted reference to a data entity
|
||||
:returns: A list of byte arrays
|
||||
"""
|
||||
data = []
|
||||
if isinstance(design_ref, str):
|
||||
design_ref = [design_ref]
|
||||
|
||||
for l in design_ref:
|
||||
try:
|
||||
LOG.debug("Resolving reference %s." % l)
|
||||
design_uri = urllib.parse.urlparse(l)
|
||||
|
||||
# when scheme is a empty string assume it is a local
|
||||
# file path
|
||||
if design_uri.scheme == '':
|
||||
handler = cls.scheme_handlers.get('file')
|
||||
else:
|
||||
handler = cls.scheme_handlers.get(design_uri.scheme, None)
|
||||
|
||||
if handler is None:
|
||||
raise InvalidPathException(
|
||||
"Invalid reference scheme %s: no handler." %
|
||||
design_uri.scheme)
|
||||
else:
|
||||
# Have to do a little magic to call the classmethod
|
||||
# as a pointer
|
||||
data.append(handler.__get__(None, cls)(design_uri))
|
||||
except ValueError:
|
||||
raise InvalidPathException(
|
||||
"Cannot resolve design reference %s: unable "
|
||||
"to parse as valid URI."
|
||||
% l)
|
||||
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def resolve_reference_http(cls, design_uri):
|
||||
"""Retrieve design documents from http/https endpoints.
|
||||
|
||||
Return a byte array of the response content. Support
|
||||
unsecured or basic auth
|
||||
|
||||
:param design_uri: Tuple as returned by urllib.parse
|
||||
for the design reference
|
||||
"""
|
||||
if design_uri.username is not None and design_uri.password is not None:
|
||||
response = requests.get(
|
||||
design_uri.geturl(),
|
||||
auth=(design_uri.username, design_uri.password),
|
||||
timeout=30)
|
||||
else:
|
||||
response = requests.get(design_uri.geturl(), timeout=30)
|
||||
if response.status_code >= 400:
|
||||
raise InvalidPathException(
|
||||
"Error received for HTTP reference: %d"
|
||||
% response.status_code)
|
||||
|
||||
return response.content
|
||||
|
||||
@classmethod
|
||||
def resolve_reference_file(cls, design_uri):
|
||||
"""Retrieve design documents from local file endpoints.
|
||||
|
||||
Return a byte array of the file contents
|
||||
|
||||
:param design_uri: Tuple as returned by urllib.parse for the design
|
||||
reference
|
||||
"""
|
||||
if design_uri.path != '':
|
||||
with open(design_uri.path, 'rb') as f:
|
||||
doc = f.read()
|
||||
return doc
|
||||
|
||||
@classmethod
|
||||
def resolve_reference_ucp(cls, design_uri):
|
||||
"""Retrieve artifacts from a UCP service endpoint.
|
||||
|
||||
Return a byte array of the response content. Assumes Keystone
|
||||
authentication required.
|
||||
|
||||
:param design_uri: Tuple as returned by urllib.parse for the design
|
||||
reference
|
||||
"""
|
||||
ks_sess = KeystoneUtils.get_session()
|
||||
(new_scheme, foo) = re.subn('^[^+]+\+', '', design_uri.scheme)
|
||||
url = urllib.parse.urlunparse(
|
||||
(new_scheme, design_uri.netloc, design_uri.path, design_uri.params,
|
||||
design_uri.query, design_uri.fragment))
|
||||
LOG.debug("Calling Keystone session for url %s" % str(url))
|
||||
resp = ks_sess.get(url)
|
||||
if resp.status_code >= 400:
|
||||
raise InvalidPathException(
|
||||
"Received error code for reference %s: %s - %s" %
|
||||
(url, str(resp.status_code), resp.text))
|
||||
return resp.content
|
||||
|
||||
scheme_handlers = {
|
||||
'http': resolve_reference_http,
|
||||
'file': resolve_reference_file,
|
||||
'https': resolve_reference_http,
|
||||
'deckhand+http': resolve_reference_ucp,
|
||||
'promenade+http': resolve_reference_ucp,
|
||||
}
|
@ -31,7 +31,6 @@ class Override(object):
|
||||
'''
|
||||
Retrieve yaml file as a dictionary.
|
||||
'''
|
||||
|
||||
try:
|
||||
with open(doc) as f:
|
||||
return list(yaml.safe_load_all(f.read()))
|
||||
|
@ -17,7 +17,7 @@ import mock
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
from armada.handlers import armada
|
||||
from armada.api.controller import armada as armada_api
|
||||
from armada.tests.unit.api import base
|
||||
|
||||
CONF = cfg.CONF
|
||||
@ -25,11 +25,9 @@ CONF = cfg.CONF
|
||||
|
||||
class ArmadaControllerTest(base.BaseControllerTest):
|
||||
|
||||
@mock.patch.object(armada, 'lint')
|
||||
@mock.patch.object(armada, 'Manifest')
|
||||
@mock.patch.object(armada, 'Tiller')
|
||||
def test_armada_apply_resource(self, mock_tiller, mock_manifest,
|
||||
mock_lint):
|
||||
@mock.patch.object(armada_api, 'Armada')
|
||||
@mock.patch.object(armada_api, 'ReferenceResolver')
|
||||
def test_armada_apply_resource(self, mock_resolver, mock_armada):
|
||||
"""Tests the POST /api/v1.0/apply endpoint."""
|
||||
rules = {'armada:create_endpoints': '@'}
|
||||
self.policy.set_rules(rules)
|
||||
@ -42,17 +40,62 @@ class ArmadaControllerTest(base.BaseControllerTest):
|
||||
'dry_run': 'false',
|
||||
'wait': 'false',
|
||||
'timeout': '100'}
|
||||
payload = {'file': '', 'options': options}
|
||||
|
||||
armada_options = {
|
||||
'disable_update_pre': False,
|
||||
'disable_update_post': False,
|
||||
'enable_chart_cleanup': False,
|
||||
'dry_run': False,
|
||||
'wait': False,
|
||||
'timeout': 100,
|
||||
'tiller_host': None,
|
||||
'tiller_port': 44134,
|
||||
}
|
||||
|
||||
payload_url = 'http://foo.com/test.yaml'
|
||||
payload = {'hrefs': [payload_url]}
|
||||
body = json.dumps(payload)
|
||||
expected = {'message': {'diff': [], 'install': [], 'upgrade': []}}
|
||||
|
||||
result = self.app.simulate_post(path='/api/v1.0/apply', body=body)
|
||||
mock_resolver.resolve_reference.return_value = \
|
||||
[b"---\nfoo: bar"]
|
||||
|
||||
mock_armada.return_value.sync.return_value = \
|
||||
{'diff': [], 'install': [], 'upgrade': []}
|
||||
|
||||
result = self.app.simulate_post(path='/api/v1.0/apply',
|
||||
body=body,
|
||||
headers={
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
params=options)
|
||||
self.assertEqual(result.json, expected)
|
||||
self.assertEqual('application/json', result.headers['content-type'])
|
||||
|
||||
mock_tiller.assert_called_once_with(tiller_host=None,
|
||||
tiller_port=44134)
|
||||
mock_manifest.assert_called_once_with([payload])
|
||||
mock_lint.validate_armada_documents.assert_called_once_with([payload])
|
||||
fake_manifest = mock_manifest.return_value.get_manifest.return_value
|
||||
mock_lint.validate_armada_object.assert_called_once_with(fake_manifest)
|
||||
mock_resolver.resolve_reference.assert_called_with([payload_url])
|
||||
mock_armada.assert_called_with([{'foo': 'bar'}], **armada_options)
|
||||
mock_armada.return_value.sync.assert_called()
|
||||
|
||||
def test_armada_apply_no_href(self):
|
||||
"""Tests /api/v1.0/apply returns 400 when hrefs list is empty."""
|
||||
rules = {'armada:create_endpoints': '@'}
|
||||
self.policy.set_rules(rules)
|
||||
|
||||
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'}
|
||||
payload = {'hrefs': []}
|
||||
body = json.dumps(payload)
|
||||
|
||||
result = self.app.simulate_post(path='/api/v1.0/apply',
|
||||
body=body,
|
||||
headers={
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
params=options)
|
||||
self.assertEqual(result.status_code, 400)
|
||||
|
@ -65,7 +65,7 @@ class LintTestCase(unittest.TestCase):
|
||||
schema: metadata/Document/v1
|
||||
name: example-manifest
|
||||
data:
|
||||
chart_groups:
|
||||
chart_groups:
|
||||
- example-group
|
||||
"""
|
||||
document = yaml.safe_load_all(template_manifest)
|
||||
@ -152,3 +152,17 @@ class LintTestCase(unittest.TestCase):
|
||||
document = yaml.safe_load_all(template_manifest)
|
||||
with self.assertRaises(Exception):
|
||||
lint.validate_chart_document(document)
|
||||
|
||||
def test_lint_validate_manifest_url(self):
|
||||
value = 'url'
|
||||
assert lint.validate_manifest_url(value) is False
|
||||
value = 'https://raw.githubusercontent.com/att-comdev/' \
|
||||
'armada/master/examples/simple.yaml'
|
||||
assert lint.validate_manifest_url(value) is True
|
||||
|
||||
def test_lint_validate_manifest_filepath(self):
|
||||
value = 'filepath'
|
||||
assert lint.validate_manifest_filepath(value) is False
|
||||
value = '{}/templates/valid_armada_document.yaml'.format(
|
||||
self.basepath)
|
||||
assert lint.validate_manifest_filepath(value) is True
|
||||
|
64
armada/utils/keystone.py
Normal file
64
armada/utils/keystone.py
Normal file
@ -0,0 +1,64 @@
|
||||
# Copyright 2017 AT&T Intellectual Property. All other rights reserved.
|
||||
#
|
||||
# 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.
|
||||
"""Utility functions for accessing Openstack Keystone."""
|
||||
|
||||
import os
|
||||
|
||||
from keystoneauth1.identity import v3
|
||||
from keystoneauth1 import session
|
||||
from oslo_config import cfg
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class KeystoneUtils(object):
|
||||
"""Utility methods for using Keystone."""
|
||||
|
||||
@staticmethod
|
||||
def get_session():
|
||||
"""Get an initialized keystone session.
|
||||
|
||||
Authentication is based on the keystone_authtoken
|
||||
section of the config file primarily. If that fails
|
||||
then attempt to create a session from environmental
|
||||
variables. This is for cases of the CLI needing
|
||||
a token.
|
||||
"""
|
||||
auth_info = dict()
|
||||
auth_fields = ['auth_url', 'username', 'password', 'project_id',
|
||||
'user_domain_name']
|
||||
try:
|
||||
for f in auth_fields:
|
||||
auth_info[f] = getattr(CONF.keystone_authtoken, f)
|
||||
auth = v3.Password(**auth_info)
|
||||
ks_session = session.Session(auth=auth)
|
||||
# Test the session
|
||||
ks_session.get_auth_headers()
|
||||
except Exception: # nosec this isn't a security issue
|
||||
pass
|
||||
else:
|
||||
return ks_session
|
||||
|
||||
try:
|
||||
for f in auth_fields:
|
||||
auth_info[f] = os.environ.get('os_{}'.format(f).upper())
|
||||
auth = v3.Password(**auth_info)
|
||||
ks_session = session.Session(auth=auth)
|
||||
# Test the session
|
||||
ks_session.get_auth_headers()
|
||||
except Exception:
|
||||
raise Exception('Missing credential information for Keystone.')
|
||||
|
||||
return ks_session
|
@ -12,6 +12,9 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
import requests
|
||||
|
||||
from armada.const import DOCUMENT_CHART, DOCUMENT_GROUP, DOCUMENT_MANIFEST
|
||||
from armada.const import KEYWORD_ARMADA, KEYWORD_PREFIX, KEYWORD_GROUPS, \
|
||||
KEYWORD_CHARTS, KEYWORD_RELEASE
|
||||
@ -93,3 +96,14 @@ def validate_chart_document(documents):
|
||||
KEYWORD_RELEASE, document.get('metadata').get('name')))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def validate_manifest_url(value):
|
||||
try:
|
||||
return (requests.get(value).status_code == 200)
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
def validate_manifest_filepath(value):
|
||||
return os.path.isfile(value)
|
||||
|
Loading…
Reference in New Issue
Block a user