Skip to content

Commit ab964ff

Browse files
committed
ref(port): when using a private registry then look for a PORT env var instead of auto discovering port
Rationale here is to not pull down the image since Kubernetes now owns the downloading and interaction with the Private Registry. Pulling an image down into the Deis Registry will strip away the benefits of the security given. This change moves the docker port auto-discovery into the docker-client and general port handling into Release model, making the Scheduler unaware of the inherent logic. Ports are still auto-discovered on public images but when Private Registry is used the default port is 5000 and if the user wants to use a different one then they need to do config:set PORT=3000 Logic may change in the future where a user can use Private Registry but pull into the local registry, and get port auto-discovery, at the cost of security.
1 parent e3fab08 commit ab964ff

12 files changed

Lines changed: 124 additions & 47 deletions

File tree

rootfs/api/models/app.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -333,12 +333,11 @@ def _scale_pods(self, scale_types):
333333
# fetch application port and inject into ENV Vars as needed
334334
envs['PORT'] = release.get_port(routable)
335335

336-
image = release.image
337336
kwargs = {
338337
'memory': release.config.memory,
339338
'cpu': release.config.cpu,
340339
'tags': release.config.tags,
341-
'envs': release.config.values,
340+
'envs': envs,
342341
'registry': release.config.registry,
343342
'version': "v{}".format(release.version),
344343
'replicas': replicas,
@@ -353,7 +352,7 @@ def _scale_pods(self, scale_types):
353352
self._scheduler.scale(
354353
namespace=self.id,
355354
name=self._get_job_id(scale_type),
356-
image=image,
355+
image=release.image,
357356
command=command,
358357
**kwargs
359358
)
@@ -384,14 +383,14 @@ def deploy(self, release):
384383
# only web / cmd are routable
385384
# http://docs.deis.io/en/latest/using_deis/process-types/#web-vs-cmd-process-types
386385
routable = True if scale_type in ['web', 'cmd'] else False
387-
# fetch application port and inject into ENV Vars as needed
386+
# fetch application port and inject into ENV vars as needed
388387
envs['PORT'] = release.get_port(routable)
389388

390389
deploys[scale_type] = {
391390
'memory': release.config.memory,
392391
'cpu': release.config.cpu,
393392
'tags': release.config.tags,
394-
'envs': release.config.values,
393+
'envs': envs,
395394
'registry': release.config.registry,
396395
# only used if there is no previous RC
397396
'replicas': replicas,

rootfs/api/models/release.py

Lines changed: 41 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,7 @@
33
from django.conf import settings
44
from django.db import models
55

6-
import docker
7-
from docker import Client
8-
from retrying import retry
9-
10-
from registry import publish_release, RegistryException
6+
from registry import publish_release, get_port as docker_get_port, RegistryException
117
from api.utils import dict_diff
128
from api.models import UuidAuditedModel, log_event, DeisException
139
from scheduler import KubeHTTPException
@@ -137,32 +133,50 @@ def publish(self):
137133
deis_registry = bool(self.build.source_based)
138134
publish_release(source_image, self.image, deis_registry, self.get_registry_auth())
139135

140-
@retry(stop_max_attempt_number=3, wait_fixed=1000)
141136
def get_port(self, routable=False):
142137
"""
143-
Get a port from a Docker image
138+
Get application port for a given release. If pulling from private registry
139+
then use default port or read from ENV var, otherwise attempt to pull from
140+
the docker image
144141
"""
145-
port = None
146-
# Only care about port for routable application
147-
if not routable:
148-
return port
149-
150-
if self.build.type == "buildpack":
151-
msg = "Using default port 5000 for build pack image {}".format(self.image)
152-
log_event(self.app, msg)
153-
return 5000
154-
155-
# get the target repository name and tag
156-
repo, tag = docker.utils.parse_repository_tag(self.image)
157-
158-
docker_cli = Client(version="auto")
159-
docker_cli.pull(repo, tag=tag, insecure_registry=True)
160-
image_info = docker_cli.inspect_image(self.image)
161-
if 'ExposedPorts' not in image_info['Config']:
162-
return None
142+
try:
143+
deis_registry = bool(self.build.source_based)
144+
envs = self.config.values
145+
creds = self.get_registry_auth()
146+
147+
port = None
148+
# Only care about port for routable application
149+
if not routable:
150+
return port
151+
152+
if self.build.type == "buildpack":
153+
msg = "Using default port 5000 for build pack image {}".format(self.image)
154+
log_event(self.app, msg)
155+
return 5000
156+
157+
# application has registry auth - $PORT is required
158+
if creds is not None:
159+
if envs.get('PORT', None) is None:
160+
log_event(self.app, 'Private registry detected but no $PORT defined. Defaulting to $PORT 5000', logging.WARNING) # noqa
161+
return 5000
162+
163+
# User provided PORT
164+
return envs.get('PORT')
165+
166+
# If the user provides PORT
167+
if envs.get('PORT', None):
168+
return envs.get('PORT')
169+
170+
# discover port from docker image
171+
port = docker_get_port(self.image, deis_registry, creds)
172+
if port is None:
173+
msg = "Expose a port or make the app non routable by changing the process type"
174+
log_event(self.app, msg, logging.ERROR)
175+
raise DeisException(msg)
163176

164-
port = int(list(image_info['Config']['ExposedPorts'].keys())[0].split("/")[0])
165-
return port
177+
return port
178+
except Exception as e:
179+
raise DeisException(str(e)) from e
166180

167181
def get_registry_auth(self):
168182
"""

rootfs/api/tests/test_app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def _mock_run(*args, **kwargs):
3131

3232
@requests_mock.Mocker(real_http=True, adapter=adapter)
3333
@mock.patch('api.models.release.publish_release', lambda *args: None)
34-
@mock.patch('api.models.release.Release.get_port', mock_port)
34+
@mock.patch('api.models.release.docker_get_port', mock_port)
3535
class AppTest(APITestCase):
3636
"""Tests creation of applications"""
3737

rootfs/api/tests/test_build.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
@requests_mock.Mocker(real_http=True, adapter=adapter)
2727
@mock.patch('api.models.release.publish_release', lambda *args: None)
28-
@mock.patch('api.models.release.Release.get_port', mock_port)
28+
@mock.patch('api.models.release.docker_get_port', mock_port)
2929
class BuildTest(APITransactionTestCase):
3030

3131
"""Tests build notification from build system"""

rootfs/api/tests/test_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
@requests_mock.Mocker(real_http=True, adapter=adapter)
2323
@mock.patch('api.models.release.publish_release', lambda *args: None)
24-
@mock.patch('api.models.release.Release.get_port', mock_port)
24+
@mock.patch('api.models.release.docker_get_port', mock_port)
2525
class ConfigTest(APITransactionTestCase):
2626

2727
"""Tests setting and updating config values"""

rootfs/api/tests/test_hooks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535

3636
@requests_mock.Mocker(real_http=True, adapter=adapter)
3737
@mock.patch('api.models.release.publish_release', lambda *args: None)
38-
@mock.patch('api.models.release.Release.get_port', mock_port)
38+
@mock.patch('api.models.release.docker_get_port', mock_port)
3939
class HookTest(APITransactionTestCase):
4040

4141
"""Tests API hooks used to trigger actions from external components"""

rootfs/api/tests/test_pods.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525
@requests_mock.Mocker(real_http=True, adapter=adapter)
2626
@mock.patch('api.models.release.publish_release', lambda *args: None)
27-
@mock.patch('api.models.release.Release.get_port', mock_port)
27+
@mock.patch('api.models.release.docker_get_port', mock_port)
2828
class PodTest(APITransactionTestCase):
2929
"""Tests creation of pods on nodes"""
3030

rootfs/api/tests/test_release.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
@requests_mock.Mocker(real_http=True, adapter=adapter)
2525
@mock.patch('api.models.release.publish_release', lambda *args: None)
26-
@mock.patch('api.models.release.Release.get_port', mock_port)
26+
@mock.patch('api.models.release.docker_get_port', mock_port)
2727
class ReleaseTest(APITransactionTestCase):
2828

2929
"""Tests push notification from build system"""

rootfs/registry/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
from .dockerclient import publish_release, RegistryException # noqa
1+
from .dockerclient import publish_release, get_port, RegistryException # noqa

rootfs/registry/dockerclient.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from django.conf import settings
88
from rest_framework.exceptions import PermissionDenied
99
from simpleflock import SimpleFlock
10+
from retrying import retry
1011

1112
import docker
1213
import docker.constants
@@ -59,6 +60,28 @@ def login(self, repository, creds=None):
5960

6061
logger.info('Successfully logged into {} with {}'.format(repository, registry_auth['username'])) # noqa
6162

63+
def get_port(self, target, deis_registry=False, creds=None):
64+
"""
65+
Get a port from a Docker image
66+
"""
67+
# get the target repository name and tag
68+
name, _ = docker.utils.parse_repository_tag(target)
69+
70+
# strip any "http://host.domain:port" prefix from the target repository name,
71+
# since we always publish to the Deis registry
72+
repo, name = auth.split_repo_name(name)
73+
74+
# log into pull repo
75+
if not deis_registry:
76+
self.login(repo, creds)
77+
78+
info = self.inspect_image(target, deis_registry)
79+
if 'ExposedPorts' not in info['Config']:
80+
return None
81+
82+
port = int(list(info['Config']['ExposedPorts'].keys())[0].split('/')[0])
83+
return port
84+
6285
def publish_release(self, source, target, deis_registry=False, creds=None):
6386
"""Update a source Docker image with environment config and publish it to deis-registry."""
6487
# get the source repository name and tag
@@ -121,6 +144,22 @@ def tag(self, image, repo, tag):
121144
if not self.client.tag(image, repo, tag=tag, force=True):
122145
raise RegistryException('Tagging {} as {}:{} failed'.format(image, repo, tag))
123146

147+
@retry(stop_max_attempt_number=3, wait_fixed=1000)
148+
def inspect_image(self, target, insecure_registry=True):
149+
"""
150+
Inspect docker image to gather information from it
151+
152+
try thrice to find the port before raising exception as docker-py is flaky
153+
"""
154+
# image already includes the tag, so we split it out here
155+
repo, tag = docker.utils.parse_repository_tag(target)
156+
157+
# make sure image is pulled locally already
158+
self.pull(repo, tag=tag, insecure_registry=insecure_registry)
159+
160+
# inspect the image
161+
return self.client.inspect_image(target)
162+
124163

125164
def check_blacklist(repo):
126165
"""Check a Docker repository name for collision with deis/* components."""
@@ -156,3 +195,7 @@ def stream_error(chunk, operation, repo, tag):
156195

157196
def publish_release(source, target, deis_registry, creds=None):
158197
return DockerClient().publish_release(source, target, deis_registry, creds)
198+
199+
200+
def get_port(target, deis_registry, creds=None):
201+
return DockerClient().get_port(target, deis_registry, creds)

0 commit comments

Comments
 (0)