Skip to content

Commit a823acc

Browse files
committed
Make dockerpycreds part of the SDK under docker.credentials
Signed-off-by: Joffrey F <joffrey@docker.com>
1 parent 41e1c05 commit a823acc

File tree

18 files changed

+341
-13
lines changed

18 files changed

+341
-13
lines changed

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ include LICENSE
66
recursive-include tests *.py
77
recursive-include tests/unit/testdata *
88
recursive-include tests/integration/testdata *
9+
recursive-include tests/gpg-keys *

Makefile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ clean:
88

99
.PHONY: build
1010
build:
11-
docker build -t docker-sdk-python .
11+
docker build -t docker-sdk-python -f tests/Dockerfile --build-arg PYTHON_VERSION=2.7 .
1212

1313
.PHONY: build-py3
1414
build-py3:
15-
docker build -t docker-sdk-python3 -f Dockerfile-py3 .
15+
docker build -t docker-sdk-python3 -f tests/Dockerfile .
1616

1717
.PHONY: build-docs
1818
build-docs:
@@ -39,7 +39,7 @@ integration-test: build
3939

4040
.PHONY: integration-test-py3
4141
integration-test-py3: build-py3
42-
docker run -t --rm -v /var/run/docker.sock:/var/run/docker.sock docker-sdk-python3 py.test tests/integration/${file}
42+
docker run -t --rm -v /var/run/docker.sock:/var/run/docker.sock docker-sdk-python3 py.test -v tests/integration/${file}
4343

4444
TEST_API_VERSION ?= 1.35
4545
TEST_ENGINE_VERSION ?= 17.12.0-ce

docker/auth.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
import json
33
import logging
44

5-
import dockerpycreds
65
import six
76

7+
from . import credentials
88
from . import errors
99
from .utils import config
1010

@@ -273,17 +273,17 @@ def _resolve_authconfig_credstore(self, registry, credstore_name):
273273
'Password': data['Secret'],
274274
})
275275
return res
276-
except dockerpycreds.CredentialsNotFound:
276+
except credentials.CredentialsNotFound:
277277
log.debug('No entry found')
278278
return None
279-
except dockerpycreds.StoreError as e:
279+
except credentials.StoreError as e:
280280
raise errors.DockerException(
281281
'Credentials store error: {0}'.format(repr(e))
282282
)
283283

284284
def _get_store_instance(self, name):
285285
if name not in self._stores:
286-
self._stores[name] = dockerpycreds.Store(
286+
self._stores[name] = credentials.Store(
287287
name, environment=self._credstore_env
288288
)
289289
return self._stores[name]

docker/credentials/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# flake8: noqa
2+
from .store import Store
3+
from .errors import StoreError, CredentialsNotFound
4+
from .constants import *

docker/credentials/constants.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
PROGRAM_PREFIX = 'docker-credential-'
2+
DEFAULT_LINUX_STORE = 'secretservice'
3+
DEFAULT_OSX_STORE = 'osxkeychain'
4+
DEFAULT_WIN32_STORE = 'wincred'

docker/credentials/errors.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
class StoreError(RuntimeError):
2+
pass
3+
4+
5+
class CredentialsNotFound(StoreError):
6+
pass
7+
8+
9+
class InitializationError(StoreError):
10+
pass
11+
12+
13+
def process_store_error(cpe, program):
14+
message = cpe.output.decode('utf-8')
15+
if 'credentials not found in native keychain' in message:
16+
return CredentialsNotFound(
17+
'No matching credentials in {}'.format(
18+
program
19+
)
20+
)
21+
return StoreError(
22+
'Credentials store {} exited with "{}".'.format(
23+
program, cpe.output.decode('utf-8').strip()
24+
)
25+
)

docker/credentials/store.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import json
2+
import os
3+
import subprocess
4+
5+
import six
6+
7+
from . import constants
8+
from . import errors
9+
from .utils import create_environment_dict
10+
from .utils import find_executable
11+
12+
13+
class Store(object):
14+
def __init__(self, program, environment=None):
15+
""" Create a store object that acts as an interface to
16+
perform the basic operations for storing, retrieving
17+
and erasing credentials using `program`.
18+
"""
19+
self.program = constants.PROGRAM_PREFIX + program
20+
self.exe = find_executable(self.program)
21+
self.environment = environment
22+
if self.exe is None:
23+
raise errors.InitializationError(
24+
'{} not installed or not available in PATH'.format(
25+
self.program
26+
)
27+
)
28+
29+
def get(self, server):
30+
""" Retrieve credentials for `server`. If no credentials are found,
31+
a `StoreError` will be raised.
32+
"""
33+
if not isinstance(server, six.binary_type):
34+
server = server.encode('utf-8')
35+
data = self._execute('get', server)
36+
result = json.loads(data.decode('utf-8'))
37+
38+
# docker-credential-pass will return an object for inexistent servers
39+
# whereas other helpers will exit with returncode != 0. For
40+
# consistency, if no significant data is returned,
41+
# raise CredentialsNotFound
42+
if result['Username'] == '' and result['Secret'] == '':
43+
raise errors.CredentialsNotFound(
44+
'No matching credentials in {}'.format(self.program)
45+
)
46+
47+
return result
48+
49+
def store(self, server, username, secret):
50+
""" Store credentials for `server`. Raises a `StoreError` if an error
51+
occurs.
52+
"""
53+
data_input = json.dumps({
54+
'ServerURL': server,
55+
'Username': username,
56+
'Secret': secret
57+
}).encode('utf-8')
58+
return self._execute('store', data_input)
59+
60+
def erase(self, server):
61+
""" Erase credentials for `server`. Raises a `StoreError` if an error
62+
occurs.
63+
"""
64+
if not isinstance(server, six.binary_type):
65+
server = server.encode('utf-8')
66+
self._execute('erase', server)
67+
68+
def list(self):
69+
""" List stored credentials. Requires v0.4.0+ of the helper.
70+
"""
71+
data = self._execute('list', None)
72+
return json.loads(data.decode('utf-8'))
73+
74+
def _execute(self, subcmd, data_input):
75+
output = None
76+
env = create_environment_dict(self.environment)
77+
try:
78+
if six.PY3:
79+
output = subprocess.check_output(
80+
[self.exe, subcmd], input=data_input, env=env,
81+
)
82+
else:
83+
process = subprocess.Popen(
84+
[self.exe, subcmd], stdin=subprocess.PIPE,
85+
stdout=subprocess.PIPE, env=env,
86+
)
87+
output, err = process.communicate(data_input)
88+
if process.returncode != 0:
89+
raise subprocess.CalledProcessError(
90+
returncode=process.returncode, cmd='', output=output
91+
)
92+
except subprocess.CalledProcessError as e:
93+
raise errors.process_store_error(e, self.program)
94+
except OSError as e:
95+
if e.errno == os.errno.ENOENT:
96+
raise errors.StoreError(
97+
'{} not installed or not available in PATH'.format(
98+
self.program
99+
)
100+
)
101+
else:
102+
raise errors.StoreError(
103+
'Unexpected OS error "{}", errno={}'.format(
104+
e.strerror, e.errno
105+
)
106+
)
107+
return output

docker/credentials/utils.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import distutils.spawn
2+
import os
3+
import sys
4+
5+
6+
def find_executable(executable, path=None):
7+
"""
8+
As distutils.spawn.find_executable, but on Windows, look up
9+
every extension declared in PATHEXT instead of just `.exe`
10+
"""
11+
if sys.platform != 'win32':
12+
return distutils.spawn.find_executable(executable, path)
13+
14+
if path is None:
15+
path = os.environ['PATH']
16+
17+
paths = path.split(os.pathsep)
18+
extensions = os.environ.get('PATHEXT', '.exe').split(os.pathsep)
19+
base, ext = os.path.splitext(executable)
20+
21+
if not os.path.isfile(executable):
22+
for p in paths:
23+
for ext in extensions:
24+
f = os.path.join(p, base + ext)
25+
if os.path.isfile(f):
26+
return f
27+
return None
28+
else:
29+
return executable
30+
31+
32+
def create_environment_dict(overrides):
33+
"""
34+
Create and return a copy of os.environ with the specified overrides
35+
"""
36+
result = os.environ.copy()
37+
result.update(overrides or {})
38+
return result

requirements.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ asn1crypto==0.22.0
33
backports.ssl-match-hostname==3.5.0.1
44
cffi==1.10.0
55
cryptography==2.3
6-
docker-pycreds==0.4.0
76
enum34==1.1.6
87
idna==2.5
98
ipaddress==1.0.18

setup.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
requirements = [
1313
'six >= 1.4.0',
1414
'websocket-client >= 0.32.0',
15-
'docker-pycreds >= 0.4.0',
1615
'requests >= 2.14.2, != 2.18.0',
1716
]
1817

0 commit comments

Comments
 (0)