Compare commits

...

34 commits

Author SHA1 Message Date
tifayuki
e9658aae73 bump version 2016-12-01 13:51:32 -08:00
Feng Honglin
9ffe81583c Merge pull request #19 from docker/not_permanently_cache_invalid_auth_header
CLOUD-3774 not permanently cache invalid auth header
2016-12-01 13:49:00 -08:00
tifayuki
f78ac3a399 not permanently cache invalid auth header 2016-11-30 10:22:18 -08:00
tifayuki
4325c7c68b Merge branch 'staging' 2016-08-05 12:40:37 +02:00
tifayuki
06988c332b bump ver 2016-08-05 12:40:19 +02:00
Feng Honglin
6bc702818e Merge pull request #15 from docker/credential-helpers
CLOUD-2557 Credential helpers
2016-08-05 12:06:22 +02:00
tifayuki
bc67aa43cb add support for credential-helpers 2016-08-05 12:00:30 +02:00
tifayuki
f5c85f96f1 fix test 2016-08-04 15:42:11 +02:00
Feng Honglin
56c80648a8 Merge pull request #13 from docker/staging
v1.0.7
2016-06-17 18:38:13 +02:00
Feng Honglin
2fed5ba8ea Merge pull request #12 from docker/UnnamespaceAction
TUT-1219 Unnamespace action endpoints
2016-06-17 18:36:58 +02:00
tifayuki
afe9a7dad5 bump version 2016-06-17 18:01:53 +02:00
tifayuki
9f484a8a59 remove namespace from action endpoints 2016-06-17 17:58:30 +02:00
Feng Honglin
648d107202 Merge pull request #11 from docker/staging
v1.0.6
2016-06-13 17:41:07 +02:00
Feng Honglin
13a8becdf4 Merge pull request #10 from docker/namespace_support
TUT-1170 Namespace support
2016-06-13 17:40:20 +02:00
tifayuki
ef94b85a16 exclude az,nodetype,provider,region from namespace 2016-06-10 19:33:14 +02:00
tifayuki
8b79e771d5 Update Readme 2016-06-10 16:51:14 +02:00
tifayuki
655fcce56a bump version 2016-06-10 11:22:07 +02:00
tifayuki
d2ec0d96e7 add the support for team and orgs 2016-06-09 19:58:23 +02:00
tifayuki
ac5e67267d Merge branch 'staging' 2016-05-18 11:48:02 +02:00
tifayuki
8494e4e04d bump ver 2016-05-17 19:20:39 +02:00
Feng Honglin
ce862f20c1 Merge pull request #5 from docker/auth_error
TUT-974 Fix the auth error handling in websocket
2016-05-17 18:44:33 +02:00
tifayuki
94a632aa4c fix the auth error handler error in dockercloud events 2016-05-10 13:09:09 +02:00
Feng Honglin
526e7cdbab Merge pull request #6 from docker/staging
1.0.4
2016-04-26 13:37:42 +02:00
tifayuki
ed92451219 add tests for CI 2016-04-26 13:26:29 +02:00
tifayuki
aef85ca5ec bump ver 2016-04-25 17:14:52 +02:00
tifayuki
91dfe343ed remove makefile and add test-requirements 2016-04-25 17:14:51 +02:00
tifayuki
be9d715150 change ws ping_interval to adapt new version of ws client 2016-04-19 17:44:28 +02:00
tifayuki
af47558410 use abstract dependency instead of concrete ones 2016-04-14 13:06:56 +02:00
tifayuki
078120ed59 Merge branch 'mochawich-patch-1' into staging 2016-04-14 11:53:28 +02:00
Mohamad Nour Chawich
cb9a7ce01e Update requirements.txt
Old version of websocket-client has many bugs that were fixed. Some were causing issues in dependencies like this one docker/dockercloud-haproxy#15
2016-04-13 17:08:34 +02:00
tifayuki
260e7c1355 Merge branch 'staging' 2016-03-03 18:24:20 +01:00
tifayuki
890d95e071 bump version 2016-03-03 18:23:43 +01:00
Feng Honglin
067843939b Merge pull request #2 from docker/TUT-751
fix the cookies on prepared request (TUT-751)
2016-03-03 18:17:19 +01:00
tifayuki
7da96e8a98 fix the cookies on prepared request 2016-03-03 17:26:03 +01:00
19 changed files with 130 additions and 46 deletions

8
Dockerfile Normal file
View file

@ -0,0 +1,8 @@
FROM python:2.7.11-alpine
RUN apk update && apk add ca-certificates
ADD . /sdk
WORKDIR sdk
RUN python setup.py install

View file

@ -1,3 +1,4 @@
include LICENSE include LICENSE
include requirements.txt include requirements.txt
include test-requirements.txt
include README.md include README.md

View file

@ -1,13 +0,0 @@
test:prepare
venv/bin/python setup.py test
clean:
rm -rf venv build dist *.egg-info
find . -name '*.pyc' -delete
prepare:clean
set -ex
virtualenv venv
venv/bin/pip install mock
venv/bin/pip install -r requirements.txt
venv/bin/python setup.py install

View file

@ -29,6 +29,19 @@ The authentication can be configured in the following ways:
export DOCKERCLOUD_USER=username export DOCKERCLOUD_USER=username
export DOCKERCLOUD_APIKEY=apikey export DOCKERCLOUD_APIKEY=apikey
## Namespace
To support teams and orgs, you can specify the namespace in the following ways:
* Set it in the Python code:
import dockercloud
dockercloud.namespace = "yourteam"
* Set it in the environment variable:
export DOCKERCLOUD_NAMESPACE=yourteam
## Errors ## Errors
Errors in the HTTP API will be returned with status codes in the 4xx and 5xx ranges. Errors in the HTTP API will be returned with status codes in the 4xx and 5xx ranges.

3
docker-compose.test.yml Normal file
View file

@ -0,0 +1,3 @@
sut:
build: .
command: python setup.py test

View file

@ -25,7 +25,7 @@ from dockercloud.api.utils import Utils
from dockercloud.api.events import Events from dockercloud.api.events import Events
from dockercloud.api.nodeaz import AZ from dockercloud.api.nodeaz import AZ
__version__ = '1.0.2' __version__ = '1.0.9'
dockercloud_auth = os.environ.get('DOCKERCLOUD_AUTH') dockercloud_auth = os.environ.get('DOCKERCLOUD_AUTH')
basic_auth = auth.load_from_file("~/.docker/config.json") basic_auth = auth.load_from_file("~/.docker/config.json")
@ -38,6 +38,8 @@ if os.environ.get('DOCKERCLOUD_USER') and os.environ.get('DOCKERCLOUD_APIKEY'):
rest_host = os.environ.get("DOCKERCLOUD_REST_HOST") or 'https://cloud.docker.com/' rest_host = os.environ.get("DOCKERCLOUD_REST_HOST") or 'https://cloud.docker.com/'
stream_host = os.environ.get("DOCKERCLOUD_STREAM_HOST") or 'wss://ws.cloud.docker.com/' stream_host = os.environ.get("DOCKERCLOUD_STREAM_HOST") or 'wss://ws.cloud.docker.com/'
namespace = os.environ.get('DOCKERCLOUD_NAMESPACE')
user_agent = None user_agent = None
logging.basicConfig() logging.basicConfig()

View file

@ -6,6 +6,7 @@ from .base import Immutable, StreamingLog
class Action(Immutable): class Action(Immutable):
subsystem = 'audit' subsystem = 'audit'
endpoint = "/action" endpoint = "/action"
namespaced = False
@classmethod @classmethod
def _pk_key(cls): def _pk_key(cls):

View file

@ -3,12 +3,14 @@ from __future__ import absolute_import
import base64 import base64
import json import json
import os import os
import subprocess
from requests.auth import HTTPBasicAuth from requests.auth import HTTPBasicAuth
import dockercloud import dockercloud
from .http import send_request from .http import send_request
HUB_INDEX = "https://index.docker.io/v1/"
def authenticate(username, password): def authenticate(username, password):
verify_credential(username, password) verify_credential(username, password)
@ -43,11 +45,29 @@ def load_from_file(f="~/.docker/config.json"):
try: try:
with open(os.path.expanduser(f)) as config_file: with open(os.path.expanduser(f)) as config_file:
data = json.load(config_file) data = json.load(config_file)
except:
return data.get("auths", {}).get("https://index.docker.io/v1/", {}).get("auth", None)
except Exception:
return None return None
creds_store = data.get("credsStore", None)
if creds_store:
try:
cmd = "docker-credential-" + creds_store
p = subprocess.Popen([cmd, 'get'], stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.STDOUT)
out = p.communicate(input=HUB_INDEX)[0]
except:
raise dockercloud.AuthError('error getting credentials - err: exec: "%s": executable file not found in $PATH, out: ``' % cmd)
try:
credential = json.loads(out)
username = credential.get("Username")
password = credential.get("Secret")
return base64.b64encode("%s:%s" % (username, password))
except:
return None
else:
return data.get("auths", {}).get(HUB_INDEX, {}).get("auth", None)
def get_auth_header(): def get_auth_header():
try: try:

View file

@ -22,6 +22,7 @@ class BasicObject(object):
class Restful(BasicObject): class Restful(BasicObject):
_detail_uri = None _detail_uri = None
namespaced = True
def __init__(self, **kwargs): def __init__(self, **kwargs):
"""Simply reflect all the values in kwargs""" """Simply reflect all the values in kwargs"""
@ -58,7 +59,11 @@ class Restful(BasicObject):
assert subsystem, "Subsystem not specified for %s" % self.__class__.__name__ assert subsystem, "Subsystem not specified for %s" % self.__class__.__name__
for k, v in list(dict.items()): for k, v in list(dict.items()):
setattr(self, k, v) setattr(self, k, v)
self._detail_uri = "/".join(["api", subsystem, self._api_version, endpoint.strip("/"), self.pk]) if self.namespaced and dockercloud.namespace:
self._detail_uri = "/".join(["api", subsystem, self._api_version, dockercloud.namespace,
endpoint.strip("/"), self.pk])
else:
self._detail_uri = "/".join(["api", subsystem, self._api_version, endpoint.strip("/"), self.pk])
self.__setchanges__([]) self.__setchanges__([])
@property @property
@ -126,7 +131,10 @@ class Immutable(Restful):
subsystem = getattr(cls, 'subsystem', None) subsystem = getattr(cls, 'subsystem', None)
assert endpoint, "Endpoint not specified for %s" % cls.__name__ assert endpoint, "Endpoint not specified for %s" % cls.__name__
assert subsystem, "Subsystem not specified for %s" % cls.__name__ assert subsystem, "Subsystem not specified for %s" % cls.__name__
detail_uri = "/".join(["api", subsystem, cls._api_version, endpoint.strip("/"), pk]) if cls.namespaced and dockercloud.namespace:
detail_uri = "/".join(["api", subsystem, cls._api_version, dockercloud.namespace, endpoint.strip("/"), pk])
else:
detail_uri = "/".join(["api", subsystem, cls._api_version, endpoint.strip("/"), pk])
json = send_request('GET', detail_uri) json = send_request('GET', detail_uri)
if json: if json:
instance = cls() instance = cls()
@ -141,7 +149,10 @@ class Immutable(Restful):
assert endpoint, "Endpoint not specified for %s" % cls.__name__ assert endpoint, "Endpoint not specified for %s" % cls.__name__
assert subsystem, "Subsystem not specified for %s" % cls.__name__ assert subsystem, "Subsystem not specified for %s" % cls.__name__
detail_uri = "/".join(["api", subsystem, cls._api_version, endpoint.strip("/")]) if cls.namespaced and dockercloud.namespace:
detail_uri = "/".join(["api", subsystem, cls._api_version, dockercloud.namespace, endpoint.strip("/")])
else:
detail_uri = "/".join(["api", subsystem, cls._api_version, endpoint.strip("/")])
objects = [] objects = []
while True: while True:
if limit and len(objects) >= limit: if limit and len(objects) >= limit:
@ -219,7 +230,10 @@ class Mutable(Immutable):
# Figure out whether we should do a create or update # Figure out whether we should do a create or update
if not self._detail_uri: if not self._detail_uri:
action = "POST" action = "POST"
path = "/".join(["api", subsystem, self._api_version, endpoint.lstrip("/")]) if cls.namespaced and dockercloud.namespace:
path = "/".join(["api", subsystem, self._api_version, dockercloud.namespace, endpoint.lstrip("/")])
else:
path = "/".join(["api", subsystem, self._api_version, endpoint.lstrip("/")])
else: else:
action = "PATCH" action = "PATCH"
path = self._detail_uri path = self._detail_uri
@ -253,18 +267,14 @@ class Triggerable(BasicObject):
class StreamingAPI(BasicObject): class StreamingAPI(BasicObject):
def __init__(self, url): def __init__(self, url):
self._ws_init(url)
def _ws_init(self, url):
self.url = url self.url = url
user_agent = 'python-dockercloud/%s' % dockercloud.__version__ user_agent = 'python-dockercloud/%s' % dockercloud.__version__
if dockercloud.user_agent: if dockercloud.user_agent:
user_agent = "%s %s" % (dockercloud.user_agent, user_agent) user_agent = "%s %s" % (dockercloud.user_agent, user_agent)
header = {'User-Agent': user_agent} header = {'User-Agent': user_agent}
header.update(dockercloud.auth.get_auth_header()) header.update(dockercloud.auth.get_auth_header())
self.header = [": ".join([key, value]) for key, value in header.items()] self.header = [": ".join([key, value]) for key, value in header.items()]
logger.info("websocket: %s %s" % (self.url, self.header)) logger.info("Websocket: %s %s" % (self.url, self.header))
self.open_handler = None self.open_handler = None
self.message_handler = None self.message_handler = None
self.error_handler = None self.error_handler = None
@ -308,7 +318,7 @@ class StreamingAPI(BasicObject):
on_message=self._on_message, on_message=self._on_message,
on_error=self._on_error, on_error=self._on_error,
on_close=self._on_close) on_close=self._on_close)
ws.run_forever(ping_interval=5, ping_timeout=5, *args, **kwargs) ws.run_forever(ping_interval=10, ping_timeout=5, *args, **kwargs)
class StreamingLog(StreamingAPI): class StreamingLog(StreamingAPI):
@ -316,7 +326,12 @@ class StreamingLog(StreamingAPI):
endpoint = "%s/%s/logs/?follow=%s" % (resource, uuid, str(follow).lower()) endpoint = "%s/%s/logs/?follow=%s" % (resource, uuid, str(follow).lower())
if tail: if tail:
endpoint = "%s&tail=%d" % (endpoint, tail) endpoint = "%s&tail=%d" % (endpoint, tail)
url = "/".join([dockercloud.stream_host.rstrip("/"), "api", subsystem, self._api_version, endpoint.lstrip("/")]) if dockercloud.namespace:
url = "/".join([dockercloud.stream_host.rstrip("/"), "api", subsystem, self._api_version,
dockercloud.namespace, endpoint.lstrip("/")])
else:
url = "/".join([dockercloud.stream_host.rstrip("/"), "api", subsystem, self._api_version,
endpoint.lstrip("/")])
super(self.__class__, self).__init__(url) super(self.__class__, self).__init__(url)
@staticmethod @staticmethod
@ -329,13 +344,17 @@ class StreamingLog(StreamingAPI):
on_message=self._on_message, on_message=self._on_message,
on_error=self._on_error, on_error=self._on_error,
on_close=self._on_close) on_close=self._on_close)
ws.run_forever(ping_interval=5, ping_timeout=5, *args, **kwargs) ws.run_forever(ping_interval=10, ping_timeout=5, *args, **kwargs)
class Exec(StreamingAPI): class Exec(StreamingAPI):
def __init__(self, uuid, cmd='sh'): def __init__(self, uuid, cmd='sh'):
endpoint = "container/%s/exec/?command=%s" % (uuid, urllib.quote_plus(cmd)) endpoint = "container/%s/exec/?command=%s" % (uuid, urllib.quote_plus(cmd))
url = "/".join([dockercloud.stream_host.rstrip("/"), "api", "app", self._api_version, endpoint.lstrip("/")]) if dockercloud.namespace:
url = "/".join([dockercloud.stream_host.rstrip("/"), "api", "app", self._api_version,
dockercloud.namespace, endpoint.lstrip("/")])
else:
url = "/".join([dockercloud.stream_host.rstrip("/"), "api", "app", self._api_version, endpoint.lstrip("/")])
super(self.__class__, self).__init__(url) super(self.__class__, self).__init__(url)
@staticmethod @staticmethod
@ -348,4 +367,4 @@ class Exec(StreamingAPI):
on_message=self._on_message, on_message=self._on_message,
on_error=self._on_error, on_error=self._on_error,
on_close=self._on_close) on_close=self._on_close)
ws.run_forever(ping_interval=5, ping_timeout=5, *args, **kwargs) ws.run_forever(ping_interval=10, ping_timeout=5, *args, **kwargs)

View file

@ -1,6 +1,7 @@
from __future__ import absolute_import from __future__ import absolute_import
import json import json
import logging
import websocket import websocket
@ -8,35 +9,47 @@ import dockercloud
from .base import StreamingAPI from .base import StreamingAPI
from .exceptions import AuthError from .exceptions import AuthError
logger = logging.getLogger("python-dockercloud")
class Events(StreamingAPI): class Events(StreamingAPI):
def __init__(self): def __init__(self):
endpoint = "events" endpoint = "events"
url = "/".join([dockercloud.stream_host.rstrip("/"), "api", "audit", self._api_version, endpoint.lstrip("/")]) if dockercloud.namespace:
url = "/".join([dockercloud.stream_host.rstrip("/"), "api", "audit", self._api_version,
dockercloud.namespace, endpoint.lstrip("/")])
else:
url = "/".join([dockercloud.stream_host.rstrip("/"), "api", "audit", self._api_version,
endpoint.lstrip("/")])
super(self.__class__, self).__init__(url) super(self.__class__, self).__init__(url)
def _on_message(self, ws, message): def _on_message(self, ws, message):
logger.info("Websocket Message: %s" % message)
try: try:
event = json.loads(message) event = json.loads(message)
except ValueError: except ValueError:
return return
if event.get("type") == "error" and event.get("data", {}).get("errorMessage") == "UNAUTHORIZED":
self.auth_error = True
raise AuthError("Not authorized")
if event.get("type") == "auth": if event.get("type") == "auth":
return return
if self.message_handler: if self.message_handler:
self.message_handler(message) self.message_handler(message)
def _on_error(self, ws, e):
if isinstance(e, websocket._exceptions.WebSocketBadStatusException) and getattr(e, "status_code") == 401:
self.auth_error = True
super(self.__class__, self)._on_error(ws, e)
def run_forever(self, *args, **kwargs): def run_forever(self, *args, **kwargs):
while True: while True:
if self.auth_error: if self.auth_error:
raise AuthError("Not authorized") self.auth_error = False
raise AuthError("Not Authorized")
ws = websocket.WebSocketApp(self.url, header=self.header, ws = websocket.WebSocketApp(self.url, header=self.header,
on_open=self._on_open, on_open=self._on_open,
on_message=self._on_message, on_message=self._on_message,
on_error=self._on_error, on_error=self._on_error,
on_close=self._on_close) on_close=self._on_close)
ws.run_forever(ping_interval=5, ping_timeout=5, *args, **kwargs) ws.run_forever(ping_interval=10, ping_timeout=5, *args, **kwargs)

View file

@ -47,14 +47,15 @@ def send_request(method, path, inject_header=True, **kwargs):
# construct request # construct request
s = get_session() s = get_session()
req = Request(method, url, headers=headers, **kwargs) request = Request(method, url, headers=headers, **kwargs)
# get environment proxies # get environment proxies
env_proxies = utils.get_environ_proxies(url) or {} env_proxies = utils.get_environ_proxies(url) or {}
kw_args = {'proxies': env_proxies} kw_args = {'proxies': env_proxies}
# make the request # make the request
logger.info("Request: %s, %s, %s, %s, %s" % (method, url, headers, s.cookies, kwargs)) req = s.prepare_request(request)
response = s.send(req.prepare(), **kw_args) logger.info("Prepared Request: %s, %s, %s, %s" % (req.method, req.url, req.headers, kwargs))
response = s.send(req, **kw_args)
status_code = getattr(response, 'status_code', None) status_code = getattr(response, 'status_code', None)
logger.info("Response: Status %s, %s, %s" % (str(status_code), response.headers, response.text)) logger.info("Response: Status %s, %s, %s" % (str(status_code), response.headers, response.text))

View file

@ -6,6 +6,7 @@ from .base import Immutable
class AZ(Immutable): class AZ(Immutable):
subsystem = "infra" subsystem = "infra"
endpoint = "/az" endpoint = "/az"
namespaced = False
@classmethod @classmethod
def _pk_key(cls): def _pk_key(cls):

View file

@ -6,6 +6,7 @@ from .base import Immutable
class Provider(Immutable): class Provider(Immutable):
subsystem = "infra" subsystem = "infra"
endpoint = "/provider" endpoint = "/provider"
namespaced = False
@classmethod @classmethod
def _pk_key(cls): def _pk_key(cls):

View file

@ -6,6 +6,7 @@ from .base import Immutable
class Region(Immutable): class Region(Immutable):
subsystem = "infra" subsystem = "infra"
endpoint = "/region" endpoint = "/region"
namespaced = False
@classmethod @classmethod
def _pk_key(cls): def _pk_key(cls):

View file

@ -6,6 +6,7 @@ from .base import Immutable
class NodeType(Immutable): class NodeType(Immutable):
subsystem = "infra" subsystem = "infra"
endpoint = "/nodetype" endpoint = "/nodetype"
namespaced = False
@classmethod @classmethod
def _pk_key(cls): def _pk_key(cls):

2
hooks/push Executable file
View file

@ -0,0 +1,2 @@
#!/bin/sh
echo "Skipping push the image"

View file

@ -1,5 +1,4 @@
backports.ssl-match-hostname==3.4.0.2
future==0.15.0 future==0.15.0
requests==2.7.0 requests==2.7.0
six==1.9.0 six==1.9.0
websocket-client==0.32.0 websocket-client==0.37.0

View file

@ -4,6 +4,13 @@ import re
from setuptools import setup, find_packages from setuptools import setup, find_packages
requirements =[
"future >= 0.15.0, < 1",
"requests >= 2.5.2, < 3",
"six >= 1.3.0, < 2",
"websocket-client >= 0.32.0, < 1"
]
def read(*parts): def read(*parts):
path = os.path.join(os.path.dirname(__file__), *parts) path = os.path.join(os.path.dirname(__file__), *parts)
@ -19,15 +26,16 @@ def find_version(*file_paths):
return version_match.group(1) return version_match.group(1)
raise RuntimeError('Unable to find version string.') raise RuntimeError('Unable to find version string.')
with open('./test-requirements.txt') as test_reqs_txt:
test_requirements = [line for line in test_reqs_txt]
with open('requirements.txt') as f:
install_requires = f.read().splitlines()
setup( setup(
name="python-dockercloud", name="python-dockercloud",
version=find_version('dockercloud', '__init__.py'), version=find_version('dockercloud', '__init__.py'),
packages=find_packages(), packages=find_packages(),
install_requires=install_requires, install_requires=requirements,
tests_require=test_requirements,
provides=['docker'], provides=['docker'],
include_package_data=True, include_package_data=True,
author="Docker, Inc.", author="Docker, Inc.",

3
test-requirements.txt Normal file
View file

@ -0,0 +1,3 @@
mock==1.0.1
coverage==4.0.3
nose==1.3.7