default requests to cosmos to version 2, with a version 1 fallback (#658)

This commit is contained in:
tamarrow
2016-06-30 16:27:56 -07:00
committed by GitHub
parent 3712b47e1c
commit cf5a48fa04
3 changed files with 153 additions and 39 deletions

View File

@@ -1,11 +1,12 @@
import base64
import collections
import functools
import six
from dcos import emitting, http, util
from dcos.errors import (DCOSAuthenticationException,
DCOSAuthorizationException, DCOSException,
DCOSHTTPException, DefaultError)
DCOSAuthorizationException, DCOSBadRequest,
DCOSException, DCOSHTTPException, DefaultError)
from six.moves import urllib
@@ -77,6 +78,35 @@ class Cosmos():
return response.status_code == 200
def _request_preferences(self):
"""Returns dict of requests and a list of their content-type,
in preference order. Ex: "request-name" -> ["v2-request", "v1-request"]
:rtype: dict
"""
return {
"describe": [
_get_cosmos_header("describe", "v2"),
_get_cosmos_header("describe", "v1")
],
"install": [
_get_cosmos_header("install", "v2"),
_get_cosmos_header("install", "v1")
],
"list": [
_get_cosmos_header("list", "v1")
],
"list-versions": [_get_cosmos_header("list-versions", "v1")],
"render": [_get_cosmos_header("render", "v1")],
"repository/add": [_get_cosmos_header("repository/add", "v1")],
"repository/delete": [
_get_cosmos_header("repository/delete", "v1")
],
"repository/list": [_get_cosmos_header("repository/list", "v1")],
"search": [_get_cosmos_header("search", "v1")],
"uninstall": [_get_cosmos_header("uninstall", "v1")],
}
def install_app(self, pkg, options, app_id):
"""Installs a package's application
@@ -155,7 +185,6 @@ class Cosmos():
:param package_version: version of package
:type package_version: str | None
:rtype: PackageVersion
"""
return CosmosPackageVersion(package_name, package_version,
@@ -257,7 +286,7 @@ class Cosmos():
content_type = response.headers.get('Content-Type')
if content_type is None:
raise DCOSHTTPException(response)
elif _get_header("error") in content_type:
elif _get_header("error", "v1") in content_type:
logger.debug("Error: {}".format(response.json()))
error_msg = _format_error_message(response.json())
raise DCOSException(error_msg)
@@ -266,6 +295,48 @@ class Cosmos():
return check_for_cosmos_error
@cosmos_error
def _post(self, request, params, headers=None):
"""Request to cosmos server
:param request: type of request
:type requet: str
:param params: body of request
:type params: dict
:param headers: list of headers for request in order of preference
:type headers: [str]
:returns: Response
:rtype: Response
"""
url = urllib.parse.urljoin(self.cosmos_url,
'package/{}'.format(request))
if headers is None:
headers = self._request_preferences().get(request)
try:
header_preference = headers.pop(0)
version = header_preference.get("Accept").split("version=")[1]
response = http.post(url, json=params,
headers=header_preference)
if not _check_cosmos_header(request, response, version):
raise DCOSException(
"Server returned incorrect response type: {}".format(
response.headers))
except DCOSAuthenticationException:
raise
except DCOSAuthorizationException:
raise
except DCOSBadRequest as e:
if len(headers) > 0:
response = self._post(request, params, headers)
else:
response = e.response
except DCOSHTTPException as e:
# let non authentication responses be handled by `cosmos_error` so
# we can expose errors reported by cosmos
response = e.response
return response
def cosmos_post(self, request, params):
"""Request to cosmos server
@@ -277,25 +348,7 @@ class Cosmos():
:rtype: Response
"""
url = urllib.parse.urljoin(self.cosmos_url,
'package/{}'.format(request))
try:
response = http.post(url, json=params,
headers=_get_cosmos_header(request))
if not _check_cosmos_header(request, response):
raise DCOSException(
"Server returned incorrect response type: {}".format(
response.headers))
except DCOSAuthenticationException:
raise
except DCOSAuthorizationException:
raise
except DCOSHTTPException as e:
# let non authentication responses be handled by `cosmos_error` so
# we can expose errors reported by cosmos
response = e.response
return response
return self._post(request, params)
class CosmosPackageVersion():
@@ -311,13 +364,29 @@ class CosmosPackageVersion():
response = Cosmos(url).cosmos_post("describe", params)
package_info = response.json()
self._package_json = package_info.get("package")
self._package_version = package_version or \
self._package_json.get("version")
self._config_json = package_info.get("config")
self._command_json = package_info.get("command")
self._resource_json = package_info.get("resource")
self._marathon_template = package_info.get("marathonMustache")
if package_info.get("marathonMustache") is not None:
self._marathon_template = package_info["marathonMustache"]
else:
self._marathon_template = package_info.get("marathon")
if self._marathon_template is not None:
self._marathon_template = base64.b64decode(
self._marathon_template.get("v2AppMustacheTemplate")
).decode('utf-8')
if package_info.get("package") is not None:
self._package_json = package_info["package"]
self._package_version = self._package_json["version"]
else:
self._package_json = _v2_package_to_v1_package_json(package_info)
self._package_version = self._package_json["version"]
self._package_version = package_version or\
self._package_json.get("version")
def registry(self):
"""Cosmos only supports one registry right now, so default to cosmos
@@ -418,11 +487,12 @@ class CosmosPackageVersion():
return response.json().get("marathonJson")
def has_mustache_definition(self):
"""Dummy method since all packages in cosmos must have mustache
definition.
"""Returns True if packages has a marathon template
:rtype: bool
"""
return True
return self._marathon_template is not None
def options(self, user_options):
"""Makes sure user supplied options are valid, and returns valid options
@@ -480,31 +550,37 @@ class CosmosPackageVersion():
return list(response.json().get("results").keys())
def _get_header(request_type):
def _get_header(request_type, version):
"""Returns header str for talking with cosmos
:param request_type: name of specified request (ie uninstall-request)
:type request_type: str
:param verison: version of request
:type version: str
:returns: header information
:rtype: str
"""
return ("application/vnd.dcos.package.{}+json;"
"charset=utf-8;version=v1").format(request_type)
"charset=utf-8;version={}").format(request_type, version)
def _get_cosmos_header(request_name):
def _get_cosmos_header(request_name, version):
"""Returns header fields needed for a valid request to cosmos
:param request_name: name of specified request (ie uninstall)
:type request_name: str
:param verison: version of request
:type version: str
:returns: dict of required headers
:rtype: {}
"""
request_name = request_name.replace("/", ".")
return {"Accept": _get_header("{}-response".format(request_name)),
"Content-Type": _get_header("{}-request".format(request_name))}
return {"Accept": _get_header("{}-response".format(request_name),
version),
"Content-Type": _get_header("{}-request".format(request_name),
"v1")}
def _get_capabilities_header():
@@ -518,20 +594,22 @@ def _get_capabilities_header():
return {"Accept": header, "Content-Type": header}
def _check_cosmos_header(request_name, response):
def _check_cosmos_header(request_name, response, version):
"""Validate that cosmos returned correct header for request
:param request_type: name of specified request (ie uninstall-request)
:type request_type: str
:param response: response object
:type response: Response
:param verison: version of request
:type version: str
:returns: whether or not we got expected response
:rtype: bool
"""
request_name = request_name.replace("/", ".")
rsp = "{}-response".format(request_name)
return _get_header(rsp) in response.headers.get('Content-Type')
return _get_header(rsp, version) in response.headers.get('Content-Type')
def _format_error_message(error):
@@ -604,3 +682,24 @@ def _format_marathon_bad_response_message(error):
isinstance(err["errors"], collections.Sequence):
error_messages += err["errors"]
return "\n".join(error_messages)
def _v2_package_to_v1_package_json(package_info):
"""Convert v2 package information to only contain info consumed by
package.json
:param package_info: package information
:type package_info: dict
:rtype {}
"""
package_json = package_info
if "command" in package_json:
del package_json["command"]
if "config" in package_json:
del package_json["config"]
if "marathon" in package_json:
del package_json["marathon"]
if "resource" in package_json:
del package_json["resource"]
return package_json

View File

@@ -50,6 +50,19 @@ class DCOSAuthorizationException(DCOSHTTPException):
return "You are not authorized to perform this operation"
class DCOSBadRequest(DCOSHTTPException):
"""A wrapper around Response objects for HTTP Bad Request (400).
:param response: requests Response object
:type response: Response
"""
def __init__(self, response):
self.response = response
def __str__(self):
return "Bad request"
class Error(object):
"""Abstract class for describing errors."""

View File

@@ -5,8 +5,8 @@ import threading
import requests
from dcos import config, util
from dcos.errors import (DCOSAuthenticationException,
DCOSAuthorizationException, DCOSException,
DCOSHTTPException)
DCOSAuthorizationException, DCOSBadRequest,
DCOSException, DCOSHTTPException)
from requests.auth import AuthBase, HTTPBasicAuth
from six.moves import urllib
@@ -229,6 +229,8 @@ def request(method,
return response
elif response.status_code == 403:
raise DCOSAuthorizationException(response)
elif response.status_code == 400:
raise DCOSBadRequest(response)
else:
raise DCOSHTTPException(response)