From d2ec0d96e75f7a007447f7389e251c85dd4521ba Mon Sep 17 00:00:00 2001 From: tifayuki Date: Thu, 9 Jun 2016 19:58:23 +0200 Subject: [PATCH 01/11] add the support for team and orgs --- dockercloud/__init__.py | 2 ++ dockercloud/api/base.py | 34 ++++++++++++++++++++++++++++------ dockercloud/api/events.py | 7 ++++++- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/dockercloud/__init__.py b/dockercloud/__init__.py index 1fd6d63..e970478 100644 --- a/dockercloud/__init__.py +++ b/dockercloud/__init__.py @@ -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/' stream_host = os.environ.get("DOCKERCLOUD_STREAM_HOST") or 'wss://ws.cloud.docker.com/' +namespace = os.environ.get('DOCKERCLOUD_NAMESPACE') + user_agent = None logging.basicConfig() diff --git a/dockercloud/api/base.py b/dockercloud/api/base.py index c6bf3d0..afe32fd 100644 --- a/dockercloud/api/base.py +++ b/dockercloud/api/base.py @@ -58,7 +58,11 @@ class Restful(BasicObject): assert subsystem, "Subsystem not specified for %s" % self.__class__.__name__ for k, v in list(dict.items()): setattr(self, k, v) - self._detail_uri = "/".join(["api", subsystem, self._api_version, endpoint.strip("/"), self.pk]) + if 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__([]) @property @@ -126,7 +130,10 @@ class Immutable(Restful): subsystem = getattr(cls, 'subsystem', None) assert endpoint, "Endpoint 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 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) if json: instance = cls() @@ -141,7 +148,10 @@ class Immutable(Restful): assert endpoint, "Endpoint 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 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 = [] while True: if limit and len(objects) >= limit: @@ -219,7 +229,10 @@ class Mutable(Immutable): # Figure out whether we should do a create or update if not self._detail_uri: action = "POST" - path = "/".join(["api", subsystem, self._api_version, endpoint.lstrip("/")]) + if dockercloud.namespace: + path = "/".join(["api", subsystem, self._api_version, dockercloud.namespace, endpoint.lstrip("/")]) + else: + path = "/".join(["api", subsystem, self._api_version, endpoint.lstrip("/")]) else: action = "PATCH" path = self._detail_uri @@ -316,7 +329,12 @@ class StreamingLog(StreamingAPI): endpoint = "%s/%s/logs/?follow=%s" % (resource, uuid, str(follow).lower()) if 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) @staticmethod @@ -335,7 +353,11 @@ class StreamingLog(StreamingAPI): class Exec(StreamingAPI): def __init__(self, uuid, cmd='sh'): 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) @staticmethod diff --git a/dockercloud/api/events.py b/dockercloud/api/events.py index f84e4aa..06f989a 100644 --- a/dockercloud/api/events.py +++ b/dockercloud/api/events.py @@ -15,7 +15,12 @@ logger = logging.getLogger("python-dockercloud") class Events(StreamingAPI): def __init__(self): 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("/")]) self.invaid_auth_headers = set() self.auth_error = "" super(self.__class__, self).__init__(url) From 655fcce56abd0d3f0da9b52e911636d931157443 Mon Sep 17 00:00:00 2001 From: tifayuki Date: Fri, 10 Jun 2016 11:22:07 +0200 Subject: [PATCH 02/11] bump version --- dockercloud/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dockercloud/__init__.py b/dockercloud/__init__.py index e970478..12bb373 100644 --- a/dockercloud/__init__.py +++ b/dockercloud/__init__.py @@ -25,7 +25,7 @@ from dockercloud.api.utils import Utils from dockercloud.api.events import Events from dockercloud.api.nodeaz import AZ -__version__ = '1.0.5' +__version__ = '1.0.6' dockercloud_auth = os.environ.get('DOCKERCLOUD_AUTH') basic_auth = auth.load_from_file("~/.docker/config.json") From 8b79e771d5dadccc093190d1ac839ca2343c663a Mon Sep 17 00:00:00 2001 From: tifayuki Date: Fri, 10 Jun 2016 16:51:14 +0200 Subject: [PATCH 03/11] Update Readme --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 5a82ce2..aea721e 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,19 @@ The authentication can be configured in the following ways: export DOCKERCLOUD_USER=username 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 in the HTTP API will be returned with status codes in the 4xx and 5xx ranges. From ef94b85a16eec02f6ff70a94d1d7d7f99e970a16 Mon Sep 17 00:00:00 2001 From: tifayuki Date: Fri, 10 Jun 2016 19:33:14 +0200 Subject: [PATCH 04/11] exclude az,nodetype,provider,region from namespace --- dockercloud/api/base.py | 9 +++++---- dockercloud/api/nodeaz.py | 1 + dockercloud/api/nodeprovider.py | 1 + dockercloud/api/noderegion.py | 1 + dockercloud/api/nodetype.py | 1 + 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/dockercloud/api/base.py b/dockercloud/api/base.py index afe32fd..e9feeed 100644 --- a/dockercloud/api/base.py +++ b/dockercloud/api/base.py @@ -22,6 +22,7 @@ class BasicObject(object): class Restful(BasicObject): _detail_uri = None + namespaced = True def __init__(self, **kwargs): """Simply reflect all the values in kwargs""" @@ -58,7 +59,7 @@ class Restful(BasicObject): assert subsystem, "Subsystem not specified for %s" % self.__class__.__name__ for k, v in list(dict.items()): setattr(self, k, v) - if dockercloud.namespace: + if self.namespaced and dockercloud.namespace: self._detail_uri = "/".join(["api", subsystem, self._api_version, dockercloud.namespace, endpoint.strip("/"), self.pk]) else: @@ -130,7 +131,7 @@ class Immutable(Restful): subsystem = getattr(cls, 'subsystem', None) assert endpoint, "Endpoint not specified for %s" % cls.__name__ assert subsystem, "Subsystem not specified for %s" % cls.__name__ - if dockercloud.namespace: + 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]) @@ -148,7 +149,7 @@ class Immutable(Restful): assert endpoint, "Endpoint not specified for %s" % cls.__name__ assert subsystem, "Subsystem not specified for %s" % cls.__name__ - if dockercloud.namespace: + 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("/")]) @@ -229,7 +230,7 @@ class Mutable(Immutable): # Figure out whether we should do a create or update if not self._detail_uri: action = "POST" - if dockercloud.namespace: + 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("/")]) diff --git a/dockercloud/api/nodeaz.py b/dockercloud/api/nodeaz.py index abc4d5f..25a153b 100644 --- a/dockercloud/api/nodeaz.py +++ b/dockercloud/api/nodeaz.py @@ -6,6 +6,7 @@ from .base import Immutable class AZ(Immutable): subsystem = "infra" endpoint = "/az" + namespaced = False @classmethod def _pk_key(cls): diff --git a/dockercloud/api/nodeprovider.py b/dockercloud/api/nodeprovider.py index 3d31f06..168bc72 100644 --- a/dockercloud/api/nodeprovider.py +++ b/dockercloud/api/nodeprovider.py @@ -6,6 +6,7 @@ from .base import Immutable class Provider(Immutable): subsystem = "infra" endpoint = "/provider" + namespaced = False @classmethod def _pk_key(cls): diff --git a/dockercloud/api/noderegion.py b/dockercloud/api/noderegion.py index 4d10bed..9a9c97a 100644 --- a/dockercloud/api/noderegion.py +++ b/dockercloud/api/noderegion.py @@ -6,6 +6,7 @@ from .base import Immutable class Region(Immutable): subsystem = "infra" endpoint = "/region" + namespaced = False @classmethod def _pk_key(cls): diff --git a/dockercloud/api/nodetype.py b/dockercloud/api/nodetype.py index 5274aaf..7416d63 100644 --- a/dockercloud/api/nodetype.py +++ b/dockercloud/api/nodetype.py @@ -6,6 +6,7 @@ from .base import Immutable class NodeType(Immutable): subsystem = "infra" endpoint = "/nodetype" + namespaced = False @classmethod def _pk_key(cls): From 9f484a8a59a044cccd58d05a091cfb260c4d3ef3 Mon Sep 17 00:00:00 2001 From: tifayuki Date: Fri, 17 Jun 2016 17:58:30 +0200 Subject: [PATCH 05/11] remove namespace from action endpoints --- dockercloud/api/action.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dockercloud/api/action.py b/dockercloud/api/action.py index 71a3ed8..214827f 100644 --- a/dockercloud/api/action.py +++ b/dockercloud/api/action.py @@ -6,6 +6,7 @@ from .base import Immutable, StreamingLog class Action(Immutable): subsystem = 'audit' endpoint = "/action" + namespaced = False @classmethod def _pk_key(cls): From afe9a7dad5e9f23ba55ffa531f303c64107a0e5b Mon Sep 17 00:00:00 2001 From: tifayuki Date: Fri, 17 Jun 2016 18:01:53 +0200 Subject: [PATCH 06/11] bump version --- dockercloud/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dockercloud/__init__.py b/dockercloud/__init__.py index 12bb373..087fa14 100644 --- a/dockercloud/__init__.py +++ b/dockercloud/__init__.py @@ -25,7 +25,7 @@ from dockercloud.api.utils import Utils from dockercloud.api.events import Events from dockercloud.api.nodeaz import AZ -__version__ = '1.0.6' +__version__ = '1.0.7' dockercloud_auth = os.environ.get('DOCKERCLOUD_AUTH') basic_auth = auth.load_from_file("~/.docker/config.json") From f5c85f96f140343db7c163da312b56e952022c37 Mon Sep 17 00:00:00 2001 From: tifayuki Date: Thu, 4 Aug 2016 15:42:11 +0200 Subject: [PATCH 07/11] fix test --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index d20508e..1794fc6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,7 @@ FROM python:2.7.11-alpine +RUN apk update && apk add ca-certificates + ADD . /sdk WORKDIR sdk RUN python setup.py install From bc67aa43cbb5134f76cdca8416ae3e2b3f61e620 Mon Sep 17 00:00:00 2001 From: tifayuki Date: Thu, 4 Aug 2016 13:58:29 +0200 Subject: [PATCH 08/11] add support for credential-helpers --- dockercloud/api/auth.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/dockercloud/api/auth.py b/dockercloud/api/auth.py index 5c44e9c..65d6668 100644 --- a/dockercloud/api/auth.py +++ b/dockercloud/api/auth.py @@ -3,12 +3,14 @@ from __future__ import absolute_import import base64 import json import os +import subprocess from requests.auth import HTTPBasicAuth import dockercloud from .http import send_request +HUB_INDEX = "https://index.docker.io/v1/" def authenticate(username, password): verify_credential(username, password) @@ -43,11 +45,29 @@ def load_from_file(f="~/.docker/config.json"): try: with open(os.path.expanduser(f)) as config_file: data = json.load(config_file) - - return data.get("auths", {}).get("https://index.docker.io/v1/", {}).get("auth", None) - except Exception: + except: 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(): try: From 06988c332b9fd4cf2413ad7cb90b5108fb7c5cde Mon Sep 17 00:00:00 2001 From: tifayuki Date: Fri, 5 Aug 2016 12:40:19 +0200 Subject: [PATCH 09/11] bump ver --- dockercloud/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dockercloud/__init__.py b/dockercloud/__init__.py index 087fa14..85a5a80 100644 --- a/dockercloud/__init__.py +++ b/dockercloud/__init__.py @@ -25,7 +25,7 @@ from dockercloud.api.utils import Utils from dockercloud.api.events import Events from dockercloud.api.nodeaz import AZ -__version__ = '1.0.7' +__version__ = '1.0.8' dockercloud_auth = os.environ.get('DOCKERCLOUD_AUTH') basic_auth = auth.load_from_file("~/.docker/config.json") From f78ac3a39909aec3c69419aadf82c1b528c4ee92 Mon Sep 17 00:00:00 2001 From: tifayuki Date: Tue, 29 Nov 2016 14:49:51 -0800 Subject: [PATCH 10/11] not permanently cache invalid auth header --- dockercloud/api/base.py | 4 ---- dockercloud/api/events.py | 11 +++++------ 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/dockercloud/api/base.py b/dockercloud/api/base.py index e9feeed..f889f69 100644 --- a/dockercloud/api/base.py +++ b/dockercloud/api/base.py @@ -267,11 +267,7 @@ class Triggerable(BasicObject): class StreamingAPI(BasicObject): def __init__(self, url): - self._ws_init(url) - - def _ws_init(self, url): self.url = url - user_agent = 'python-dockercloud/%s' % dockercloud.__version__ if dockercloud.user_agent: user_agent = "%s %s" % (dockercloud.user_agent, user_agent) diff --git a/dockercloud/api/events.py b/dockercloud/api/events.py index 06f989a..4440f72 100644 --- a/dockercloud/api/events.py +++ b/dockercloud/api/events.py @@ -21,8 +21,6 @@ class Events(StreamingAPI): else: url = "/".join([dockercloud.stream_host.rstrip("/"), "api", "audit", self._api_version, endpoint.lstrip("/")]) - self.invaid_auth_headers = set() - self.auth_error = "" super(self.__class__, self).__init__(url) def _on_message(self, ws, message): @@ -39,15 +37,16 @@ class Events(StreamingAPI): def _on_error(self, ws, e): if isinstance(e, websocket._exceptions.WebSocketBadStatusException) and getattr(e, "status_code") == 401: - self.auth_error = "Not Authorized" - self.invaid_auth_headers.add(str(dockercloud.auth.get_auth_header())) + self.auth_error = True super(self.__class__, self)._on_error(ws, e) def run_forever(self, *args, **kwargs): while True: - if str(dockercloud.auth.get_auth_header()) in self.invaid_auth_headers: - raise AuthError(self.auth_error) + if self.auth_error: + self.auth_error = False + raise AuthError("Not Authorized") + ws = websocket.WebSocketApp(self.url, header=self.header, on_open=self._on_open, on_message=self._on_message, From e9658aae73be9e8d8a23e2d88f2da551cb293d41 Mon Sep 17 00:00:00 2001 From: tifayuki Date: Thu, 1 Dec 2016 13:51:32 -0800 Subject: [PATCH 11/11] bump version --- dockercloud/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dockercloud/__init__.py b/dockercloud/__init__.py index 85a5a80..8bb6aa5 100644 --- a/dockercloud/__init__.py +++ b/dockercloud/__init__.py @@ -25,7 +25,7 @@ from dockercloud.api.utils import Utils from dockercloud.api.events import Events from dockercloud.api.nodeaz import AZ -__version__ = '1.0.8' +__version__ = '1.0.9' dockercloud_auth = os.environ.get('DOCKERCLOUD_AUTH') basic_auth = auth.load_from_file("~/.docker/config.json")