Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow specifying images to reuse cache from #478

Merged
merged 9 commits into from Dec 11, 2018
34 changes: 27 additions & 7 deletions repo2docker/app.py
Expand Up @@ -73,6 +73,18 @@ def _default_log_level(self):
"""
)

cache_from = List(
[],
config=True,
help="""
List of images to try & re-use cached image layers from.

Docker only tries to re-use image layers from images built locally,
not pulled from a registry. We can ask it to explicitly re-use layers
from non-locally built images by through the 'cache_from' parameter.
"""
)

buildpacks = List(
[
LegacyBinderDockerBuildPack,
Expand Down Expand Up @@ -398,6 +410,13 @@ def get_argparser(self):
help='Print the repo2docker version and exit.'
)

argparser.add_argument(
'--cache-from',
action='append',
default=[],
help=self.traits()['cache_from'].help
)

return argparser

def json_excepthook(self, etype, evalue, traceback):
Expand Down Expand Up @@ -542,6 +561,9 @@ def initialize(self, argv=None):
if args.subdir:
self.subdir = args.subdir

if args.cache_from:
self.cache_from = args.cache_from

self.environment = args.environment

def push_image(self):
Expand Down Expand Up @@ -675,13 +697,11 @@ def _get_free_port(self):
return port

def start(self):
"""Start execution of repo2docker"""
# Check if r2d can connect to docker daemon
"""Start execution of repo2docker""" # Check if r2d can connect to docker daemon
if self.build:
try:
client = docker.APIClient(version='auto',
**kwargs_from_env())
del client
api_client = docker.APIClient(version='auto',
**kwargs_from_env())
except DockerException as e:
print("Docker client initialization error. Check if docker is"
" running on the host.")
Expand Down Expand Up @@ -737,8 +757,8 @@ def start(self):
self.log.info('Using %s builder\n', bp.__class__.__name__,
extra=dict(phase='building'))

for l in picked_buildpack.build(self.output_image_spec,
self.build_memory_limit, build_args):
for l in picked_buildpack.build(api_client, self.output_image_spec,
self.build_memory_limit, build_args, self.cache_from):
if 'stream' in l:
self.log.info(l['stream'],
extra=dict(phase='building'))
Expand Down
7 changes: 3 additions & 4 deletions repo2docker/buildpacks/base.py
Expand Up @@ -449,7 +449,7 @@ def render(self):
appendix=self.appendix,
)

def build(self, image_spec, memory_limit, build_args):
def build(self, client, image_spec, memory_limit, build_args, cache_from):
tarf = io.BytesIO()
tar = tarfile.open(fileobj=tarf, mode='w')
dockerfile_tarinfo = tarfile.TarInfo("Dockerfile")
Expand Down Expand Up @@ -489,8 +489,6 @@ def _filter_tar(tar):
}
if memory_limit:
limits['memory'] = memory_limit
client = docker.APIClient(version='auto',
**docker.utils.kwargs_from_env())
for line in client.build(
fileobj=tarf,
tag=image_spec,
Expand All @@ -499,7 +497,8 @@ def _filter_tar(tar):
decode=True,
forcerm=True,
rm=True,
container_limits=limits
container_limits=limits,
cache_from=cache_from
):
yield line

Expand Down
6 changes: 3 additions & 3 deletions repo2docker/buildpacks/docker.py
Expand Up @@ -19,7 +19,7 @@ def render(self):
with open(Dockerfile) as f:
return f.read()

def build(self, image_spec, memory_limit, build_args):
def build(self, client, image_spec, memory_limit, build_args, cache_from):
"""Build a Docker image based on the Dockerfile in the source repo."""
limits = {
# Always disable memory swap for building, since mostly
Expand All @@ -28,7 +28,6 @@ def build(self, image_spec, memory_limit, build_args):
}
if memory_limit:
limits['memory'] = memory_limit
client = docker.APIClient(version='auto', **docker.utils.kwargs_from_env())
for line in client.build(
path=os.getcwd(),
dockerfile=self.binder_path(self.dockerfile),
Expand All @@ -37,6 +36,7 @@ def build(self, image_spec, memory_limit, build_args):
decode=True,
forcerm=True,
rm=True,
container_limits=limits
container_limits=limits,
cache_from=cache_from
):
yield line
4 changes: 2 additions & 2 deletions repo2docker/buildpacks/legacy/__init__.py
Expand Up @@ -83,7 +83,7 @@ def get_build_script_files(self):
'legacy/python3.frozen.yml': '/tmp/python3.frozen.yml',
}

def build(self, image_spec, memory_limit, build_args):
def build(self, client, image_spec, memory_limit, build_args, cache_from):
"""Build a legacy Docker image."""
with open(self.dockerfile, 'w') as f:
f.write(self.render())
Expand All @@ -94,7 +94,7 @@ def build(self, image_spec, memory_limit, build_args):
env_file,
)
shutil.copy(src_path, env_file)
return super().build(image_spec, memory_limit, build_args)
return super().build(client, image_spec, memory_limit, build_args, cache_from)

def detect(self):
"""Check if current repo should be built with the Legacy BuildPack.
Expand Down
82 changes: 82 additions & 0 deletions tests/test_cache_from.py
@@ -0,0 +1,82 @@
"""
Test that --cache-from is passed in to docker API properly.
"""
import os
import docker
from unittest.mock import MagicMock, patch
from repo2docker.buildpacks import BaseImage, DockerBuildPack, LegacyBinderDockerBuildPack
from tempfile import TemporaryDirectory

def test_cache_from_base(monkeypatch):
FakeDockerClient = MagicMock()
cache_from = [
'image-1:latest'
]
fake_log_value = {'stream': 'fake'}
fake_client = MagicMock(spec=docker.APIClient)
fake_client.build.return_value = iter([fake_log_value])

with TemporaryDirectory() as d:
# Test base image build pack
monkeypatch.chdir(d)
for line in BaseImage().build(fake_client, 'image-2', '1Gi', {}, cache_from):
assert line == fake_log_value
called_args, called_kwargs = fake_client.build.call_args
assert 'cache_from' in called_kwargs
assert called_kwargs['cache_from'] == cache_from



def test_cache_from_docker(monkeypatch):
FakeDockerClient = MagicMock()
cache_from = [
'image-1:latest'
]
fake_log_value = {'stream': 'fake'}
fake_client = MagicMock(spec=docker.APIClient)
fake_client.build.return_value = iter([fake_log_value])

with TemporaryDirectory() as d:
# Test docker image
with open(os.path.join(d, 'Dockerfile'), 'w') as f:
f.write('FROM scratch\n')

for line in DockerBuildPack().build(fake_client, 'image-2', '1Gi', {}, cache_from):
assert line == fake_log_value
called_args, called_kwargs = fake_client.build.call_args
assert 'cache_from' in called_kwargs
assert called_kwargs['cache_from'] == cache_from

# Test legacy docker image
with open(os.path.join(d, 'Dockerfile'), 'w') as f:
f.write('FROM andrewosh/binder-base\n')

for line in LegacyBinderDockerBuildPack().build(fake_client, 'image-2', '1Gi', {}, cache_from):
print(line)
assert line == fake_log_value
called_args, called_kwargs = fake_client.build.call_args
assert 'cache_from' in called_kwargs
assert called_kwargs['cache_from'] == cache_from


def test_cache_from_legacy(monkeypatch):
FakeDockerClient = MagicMock()
cache_from = [
'image-1:latest'
]
fake_log_value = {'stream': 'fake'}
fake_client = MagicMock(spec=docker.APIClient)
fake_client.build.return_value = iter([fake_log_value])

with TemporaryDirectory() as d:
# Test legacy docker image
with open(os.path.join(d, 'Dockerfile'), 'w') as f:
f.write('FROM andrewosh/binder-base\n')

for line in LegacyBinderDockerBuildPack().build(fake_client, 'image-2', '1Gi', {}, cache_from):
assert line == fake_log_value
called_args, called_kwargs = fake_client.build.call_args
assert 'cache_from' in called_kwargs
assert called_kwargs['cache_from'] == cache_from