Skip to content

Commit 0cb2f02

Browse files
committed
Merge pull request #649 from helgi/private_registry
feat(registry): add initial support to auth to an external Registry on per app basis
2 parents b5ba78d + ba73957 commit 0cb2f02

3 files changed

Lines changed: 178 additions & 19 deletions

File tree

rootfs/api/models/release.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,17 @@ def publish(self, source_version='latest'):
105105

106106
# If the build has a SHA, assume it's from deis-builder and in the deis-registry already
107107
if not self.build.dockerfile and not self.build.sha:
108+
# gather custom login information for registry if needed
109+
auth = None
110+
if self.config.values.get('IMAGE_AUTH_USER', None):
111+
auth = {
112+
'username': self.config.values.get('IMAGE_AUTH_USER', None),
113+
'password': self.config.values.get('IMAGE_AUTH_PASSWORD', None),
114+
'email': self.owner.email
115+
}
116+
108117
deis_registry = bool(self.build.sha)
109-
publish_release(source_image, self.image, deis_registry)
118+
publish_release(source_image, self.image, deis_registry, auth)
110119

111120
def previous(self):
112121
"""

rootfs/registry/dockerclient.py

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,36 @@ def __init__(self):
3030
self.client = docker.Client(version='auto', timeout=timeout)
3131
self.registry = settings.REGISTRY_HOST + ':' + str(settings.REGISTRY_PORT)
3232

33-
def publish_release(self, source, target, deis_registry):
33+
def login(self, repository, creds=None):
34+
"""Log into a registry if auth is provided"""
35+
if not creds:
36+
return
37+
38+
# parse out the hostname since repo variable is hostname + path
39+
registry, _ = auth.resolve_repository_name(repository)
40+
41+
registry_auth = {
42+
'username': None,
43+
'password': None,
44+
'email': None,
45+
'registry': registry
46+
}
47+
registry_auth.update(creds)
48+
49+
if not registry_auth['username'] or not registry_auth['password']:
50+
msg = 'Registry auth requires a username and a password'
51+
logger.error(msg)
52+
raise PermissionDenied(msg)
53+
54+
logger.info('Logging into Registry {} with username {}'.format(repository, registry_auth['username'])) # noqa
55+
response = self.client.login(**registry_auth)
56+
success = response.get('Status') == 'Login Succeeded' or response.get('username') == registry_auth['username'] # noqa
57+
if not success:
58+
raise PermissionDenied('Could not log into {} with username {}'.format(repository, registry_auth['username'])) # noqa
59+
60+
logger.info('Successfully logged into {} with {}'.format(repository, registry_auth['username'])) # noqa
61+
62+
def publish_release(self, source, target, deis_registry=False, creds=None):
3463
"""Update a source Docker image with environment config and publish it to deis-registry."""
3564
# get the source repository name and tag
3665
src_name, src_tag = docker.utils.parse_repository_tag(source)
@@ -49,7 +78,12 @@ def publish_release(self, source, target, deis_registry):
4978
repo = src_name
5079

5180
try:
52-
self.pull(repo, src_tag)
81+
# log into pull repo
82+
if not deis_registry:
83+
self.login(repo, creds)
84+
85+
# pull image from source repository
86+
self.pull(repo, src_tag, deis_registry)
5387

5488
# tag the image locally without the repository URL
5589
image = "{}:{}".format(src_name, src_tag)
@@ -60,20 +94,24 @@ def publish_release(self, source, target, deis_registry):
6094
except APIError as e:
6195
raise RegistryException(str(e))
6296

63-
def pull(self, repo, tag):
97+
def pull(self, repo, tag, insecure_registry=True):
6498
"""Pull a Docker image into the local storage graph."""
6599
check_blacklist(repo)
66100
logger.info("Pulling Docker image {}:{}".format(repo, tag))
67101
with SimpleFlock(self.FLOCKFILE, timeout=1200):
68-
stream = self.client.pull(repo, tag=tag, stream=True,
69-
decode=True, insecure_registry=True)
102+
stream = self.client.pull(
103+
repo, tag=tag, stream=True, decode=True,
104+
insecure_registry=insecure_registry
105+
)
70106
log_output(stream, 'pull', repo, tag)
71107

72108
def push(self, repo, tag):
73109
"""Push a local Docker image to a registry."""
74110
logger.info("Pushing Docker image {}:{}".format(repo, tag))
75-
stream = self.client.push(repo, tag=tag, stream=True, decode=True,
76-
insecure_registry=True)
111+
stream = self.client.push(
112+
repo, tag=tag, stream=True, decode=True,
113+
insecure_registry=True
114+
)
77115
log_output(stream, 'push', repo, tag)
78116

79117
def tag(self, image, repo, tag):
@@ -91,7 +129,7 @@ def check_blacklist(repo):
91129
'router', 'slugbuilder', 'slugrunner', 'workflow',
92130
]
93131
if any("deis/{}".format(c) in repo for c in blacklisted):
94-
raise PermissionDenied("Repository name {} is not allowed".format(repo))
132+
raise PermissionDenied("Repository name {} is not allowed, as it is reserved by Deis".format(repo)) # noqa
95133

96134

97135
def log_output(stream, operation, repo, tag):
@@ -116,5 +154,5 @@ def stream_error(chunk, operation, repo, tag):
116154
raise RegistryException(message)
117155

118156

119-
def publish_release(source, target, deis_registry):
120-
return DockerClient().publish_release(source, target, deis_registry)
157+
def publish_release(source, target, deis_registry, creds=None):
158+
return DockerClient().publish_release(source, target, deis_registry, creds)

rootfs/registry/tests.py

Lines changed: 120 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,11 @@
55
"""
66

77
import unittest
8-
try:
9-
from unittest import mock
10-
except ImportError:
11-
import mock
8+
from unittest import mock
129

1310
from django.conf import settings
1411
from rest_framework.exceptions import PermissionDenied
12+
from registry import publish_release, RegistryException
1513
from registry.dockerclient import DockerClient
1614

1715

@@ -24,24 +22,131 @@ def setUp(self):
2422

2523
def test_publish_release(self, mock_client):
2624
self.client = DockerClient()
27-
self.client.publish_release('ozzy/embryo:git-f2a8020', 'ozzy/embryo:v4', True)
25+
26+
# Make sure login is not called when there are no creds
27+
publish_release('ozzy/embryo:git-f2a8020', 'ozzy/embryo:v4', False)
28+
self.assertFalse(self.client.client.login.called)
29+
30+
creds = {
31+
'username': 'fake',
32+
'password': 'fake',
33+
'email': 'fake',
34+
'registry': 'quay.io'
35+
}
36+
37+
client = {}
38+
client['Status'] = 'Login Succeeded'
39+
self.client.client.login.return_value = client
40+
publish_release('ozzy/embryo:git-f2a8020', 'ozzy/embryo:v4', False, creds)
41+
self.assertTrue(self.client.client.login.called)
42+
self.assertTrue(self.client.client.pull.called)
43+
self.assertTrue(self.client.client.tag.called)
44+
self.assertTrue(self.client.client.push.called)
45+
46+
publish_release('ozzy/embryo:git-f2a8020', 'ozzy/embryo:v4', True)
2847
self.assertTrue(self.client.client.pull.called)
2948
self.assertTrue(self.client.client.tag.called)
3049
self.assertTrue(self.client.client.push.called)
50+
3151
# Test that a registry host prefix is replaced with deis-registry for the target
32-
self.client.publish_release('ozzy/embryo:git-f2a8020', 'quay.io/ozzy/embryo:v4', True)
52+
publish_release('ozzy/embryo:git-f2a8020', 'quay.io/ozzy/embryo:v4', True)
3353
docker_push = self.client.client.push
3454
docker_push.assert_called_with(
3555
'localhost:5000/ozzy/embryo', tag='v4', insecure_registry=True,
3656
decode=True, stream=True)
57+
3758
# Test that blacklisted image names can't be published
3859
with self.assertRaises(PermissionDenied):
39-
self.client.publish_release(
60+
publish_release(
4061
'deis/controller:v1.11.1', 'deis/controller:v1.11.1', True)
4162
with self.assertRaises(PermissionDenied):
42-
self.client.publish_release(
63+
publish_release(
4364
'localhost:5000/deis/controller:v1.11.1', 'deis/controller:v1.11.1', True)
4465

66+
def test_login(self, mock_client):
67+
self.client = DockerClient()
68+
69+
# success
70+
client = {}
71+
client['Status'] = 'Login Succeeded'
72+
self.client.client.login.return_value = client
73+
74+
creds = {
75+
'username': 'fake',
76+
'password': 'fake',
77+
'email': 'fake',
78+
'registry': 'quay.io'
79+
}
80+
self.client.login('quay.io/deis/foobar', creds)
81+
docker_login = self.client.client.login
82+
docker_login.assert_called_with(
83+
username='fake', password='fake',
84+
email='fake', registry='quay.io'
85+
)
86+
87+
# username matches
88+
client = {}
89+
client['username'] = 'fake'
90+
self.client.client.login.return_value = client
91+
92+
creds = {
93+
'username': 'fake',
94+
'password': 'fake',
95+
'email': 'fake',
96+
'registry': 'quay.io'
97+
}
98+
self.client.login('quay.io/deis/foobar', creds)
99+
docker_login = self.client.client.login
100+
docker_login.assert_called_with(
101+
username='fake', password='fake',
102+
email='fake', registry='quay.io'
103+
)
104+
105+
def test_login_failed(self, mock_client):
106+
self.client = DockerClient()
107+
108+
# failed login
109+
client = {}
110+
client['Status'] = 'Login Failed'
111+
self.client.client.login.return_value = client
112+
113+
creds = {
114+
'username': 'fake',
115+
'password': 'fake',
116+
'email': 'fake',
117+
'registry': 'quay.io'
118+
}
119+
120+
with self.assertRaises(PermissionDenied):
121+
self.client.login('quay.io/deis/foobar', creds)
122+
docker_login = self.client.client.login
123+
docker_login.assert_called_with(
124+
username='fake', password='fake',
125+
email='fake', registry='quay.io'
126+
)
127+
128+
def test_login_bad_creds(self, mock_client):
129+
self.client = DockerClient()
130+
131+
# missing parts of credentials
132+
with self.assertRaises(PermissionDenied):
133+
creds = {
134+
'username': 'fake',
135+
'email': 'fake',
136+
'registry': 'quay.io'
137+
}
138+
self.client.login('quay.io/deis/foobar', creds)
139+
140+
# bad credentials
141+
with self.assertRaises(PermissionDenied):
142+
creds = {
143+
'username': 'fake',
144+
'password': 'fake',
145+
'email': 'fake',
146+
'registry': 'quay.io'
147+
}
148+
self.client.login('quay.io/deis/foobar', creds)
149+
45150
def test_pull(self, mock_client):
46151
self.client = DockerClient()
47152
self.client.pull('alpine', '3.2')
@@ -67,8 +172,15 @@ def test_tag(self, mock_client):
67172
docker_tag = self.client.client.tag
68173
docker_tag.assert_called_once_with(
69174
'ozzy/embryo:git-f2a8020', 'ozzy/embryo', tag='v4', force=True)
175+
176+
# fake failed tag
177+
self.client.client.tag.return_value = False
178+
with self.assertRaises(RegistryException):
179+
self.client.tag('foo/bar:latest', 'foo/bar', 'v1.11.1')
180+
70181
# Test that blacklisted image names can't be tagged
71182
with self.assertRaises(PermissionDenied):
72183
self.client.tag('deis/controller:v1.11.1', 'deis/controller', 'v1.11.1')
184+
73185
with self.assertRaises(PermissionDenied):
74186
self.client.tag('localhost:5000/deis/controller:v1.11.1', 'deis/controller', 'v1.11.1')

0 commit comments

Comments
 (0)