Skip to content

Commit

Permalink
default requests to cosmos to version 2, with a version 1 fallback (#658
Browse files Browse the repository at this point in the history
)
  • Loading branch information
tamarrow committed Jun 30, 2016
1 parent 3712b47 commit cf5a48f
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 23 deletions.
141 changes: 120 additions & 21 deletions dcos/cosmospackage.py
@@ -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

Expand Down Expand Up @@ -77,6 +78,35 @@ def enabled(self):

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
Expand Down Expand Up @@ -155,7 +185,6 @@ def get_package_version(self, package_name, package_version):
:param package_version: version of package
:type package_version: str | None
:rtype: PackageVersion
"""

return CosmosPackageVersion(package_name, package_version,
Expand Down Expand Up @@ -257,7 +286,7 @@ def check_for_cosmos_error(*args, **kwargs):
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)
Expand All @@ -266,37 +295,61 @@ def check_for_cosmos_error(*args, **kwargs):
return check_for_cosmos_error

@cosmos_error
def cosmos_post(self, request, params):
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=_get_cosmos_header(request))
if not _check_cosmos_header(request, response):
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
:param request: type of request
:type requet: str
:param params: body of request
:type params: dict
:returns: Response
:rtype: Response
"""

return self._post(request, params)


class CosmosPackageVersion():
"""Interface to a specific package version from cosmos"""
Expand All @@ -311,13 +364,29 @@ def __init__(self, name, package_version, url):
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
Expand Down Expand Up @@ -418,11 +487,12 @@ def marathon_json(self, options):
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
Expand Down Expand Up @@ -480,31 +550,37 @@ def package_versions(self):
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():
Expand All @@ -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):
Expand Down Expand Up @@ -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
13 changes: 13 additions & 0 deletions dcos/errors.py
Expand Up @@ -50,6 +50,19 @@ def __str__(self):
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."""

Expand Down
6 changes: 4 additions & 2 deletions dcos/http.py
Expand Up @@ -5,8 +5,8 @@
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
Expand Down Expand Up @@ -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)

Expand Down

0 comments on commit cf5a48f

Please sign in to comment.