Skip to content

Commit 13d9b42

Browse files
authored
Merge pull request #22 from Autodesk/docker-input-paths
Docker input paths
2 parents e08ad79 + 55a3e42 commit 13d9b42

File tree

8 files changed

+127
-30
lines changed

8 files changed

+127
-30
lines changed

codeship-steps.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
name: check_pyversion
1616
- command: ./scripts/deploy.sh
1717
name: pypi_deploy
18-
# tests for semantic versions
19-
tag: '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?(\+[0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*)?$'
18+
# tests for (a subset of) PEP440 versions
19+
tag: '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)((a|rc|b)(0|[1-9]\d*))?$'
2020

2121

pyccc/docker_utils.py

Lines changed: 44 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,23 @@
2222
import subprocess
2323
import io
2424
import tarfile
25+
import tempfile
26+
import gzip
2527

2628
import pyccc
2729

2830
from .exceptions import DockerMachineError
31+
from . import files
2932

3033

3134
def create_provisioned_image(client, image, wdir, inputs, pull=False):
3235
build_context = create_build_context(image, inputs, wdir)
33-
tarobj = make_tar_stream(build_context)
34-
imageid = build_dfile_stream(client, tarobj, is_tar=True, pull=pull)
36+
with tempfile.NamedTemporaryFile(suffix='.tar.gz', mode='wb') as tfile:
37+
gzstream = gzip.GzipFile(fileobj=tfile, mode='wb')
38+
make_tar_stream(build_context, gzstream)
39+
gzstream.flush()
40+
tfile.flush()
41+
imageid = build_dfile_stream(client, tfile.name, pull=pull)
3542
return imageid
3643

3744

@@ -40,26 +47,38 @@ def create_build_context(image, inputs, wdir):
4047
Creates a tar archive with a dockerfile and a directory called "inputs"
4148
The Dockerfile will copy the "inputs" directory to the chosen working directory
4249
"""
43-
dockerstring = "FROM %s" % image
50+
assert os.path.isabs(wdir)
4451

52+
dockerlines = ["FROM %s" % image,
53+
"RUN mkdir -p %s" % wdir]
4554
build_context = {}
46-
if inputs:
47-
dockerstring += "\nCOPY inputs %s\n" % wdir
48-
for name, obj in inputs.items():
49-
build_context['inputs/%s' % name] = obj
50-
else:
51-
dockerstring += "\nRUN mkdir -p %s\n" % wdir
5255

56+
# This loop creates a Build Context for building the provisioned image
57+
# Each input file is assigned a unique name, in preparation for creating a tar archive
58+
if inputs:
59+
for ifile, (path, obj) in enumerate(inputs.items()):
60+
src = os.path.basename(path) + '-%s' % ifile # mangling to ensure uniqueness
61+
if os.path.isabs(path):
62+
dest = path
63+
else:
64+
dest = os.path.join(wdir, path)
65+
dockerlines.append('ADD %s %s' % (src, dest))
66+
build_context[src] = obj
67+
68+
dockerstring = '\n'.join(dockerlines)
5369
build_context['Dockerfile'] = pyccc.BytesContainer(dockerstring.encode('utf-8'))
54-
5570
return build_context
5671

5772

58-
def build_dfile_stream(client, dfilestream, is_tar=False, **kwargs):
59-
buildcmd = client.build(fileobj=dfilestream,
60-
rm=True,
61-
custom_context=is_tar,
62-
**kwargs)
73+
def build_dfile_stream(client, dfilepath, **kwargs):
74+
# dfilepath is the path to the already .tgz-archived build context
75+
76+
with open(dfilepath, 'rb') as dfilestream:
77+
buildcmd = client.build(fileobj=dfilestream,
78+
rm=True,
79+
custom_context=True,
80+
encoding='gzip',
81+
**kwargs)
6382

6483
# this blocks until the image is done building
6584
for x in buildcmd:
@@ -104,23 +123,20 @@ def _issue1134_helper(x):
104123
return s[rootbrace: endbrace+1]
105124

106125

107-
def make_tar_stream(sdict):
108-
""" Create a file-like tarfile object
126+
def make_tar_stream(build_context, buffer):
127+
""" Write a tar stream of the build context to the provided buffer
109128
110129
Args:
111-
sdict (Mapping[str, pyccc.FileReferenceBase]): dict mapping filenames to file references
112-
113-
Returns:
114-
Filelike: TarFile stream
130+
build_context (Mapping[str, pyccc.FileReferenceBase]): dict mapping filenames to file references
131+
buffer (io.BytesIO): writable binary mode buffer
115132
"""
116-
# TODO: this is currently done in memory - bad for big files!
117-
tarbuffer = io.BytesIO()
118-
tf = tarfile.TarFile(fileobj=tarbuffer, mode='w')
119-
for name, fileobj in sdict.items():
120-
tar_add_bytes(tf, name, fileobj.read('rb'))
133+
tf = tarfile.TarFile(fileobj=buffer, mode='w')
134+
for context_path, fileobj in build_context.items():
135+
if isinstance(fileobj, (files.LocalDirectoryReference, files.LocalFile)):
136+
tf.add(fileobj.localpath, arcname=context_path)
137+
else:
138+
tar_add_bytes(tf, context_path, fileobj.read('rb'))
121139
tf.close()
122-
tarbuffer.seek(0)
123-
return tarbuffer
124140

125141

126142
def tar_add_bytes(tf, filename, bytestring):

pyccc/engines/subproc.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,11 @@ def submit(self, job):
6161
self._check_job(job)
6262
if job.workingdir is None:
6363
job.workingdir = utils.make_local_temp_dir()
64+
65+
assert os.path.isabs(job.workingdir)
6466
if job.inputs:
6567
for filename, f in job.inputs.items():
68+
self._check_input_target_location(filename, job.workingdir)
6669
f.put(os.path.join(job.workingdir, filename))
6770

6871
subenv = os.environ.copy()
@@ -77,6 +80,20 @@ def submit(self, job):
7780
job._started = True
7881
return job.subproc.pid
7982

83+
@staticmethod
84+
def _check_input_target_location(filename, wdir):
85+
""" Raise error if input is being staged to a location not underneath the working dir
86+
"""
87+
p = filename
88+
if not os.path.isabs(p):
89+
p = os.path.join(wdir, p)
90+
targetpath = os.path.realpath(p)
91+
wdir = os.path.realpath(wdir)
92+
common = os.path.commonprefix([wdir, targetpath])
93+
if len(common) < len(wdir):
94+
raise ValueError(
95+
"The subprocess engine does not support input files with absolute paths")
96+
8097
def kill(self, job):
8198
job.subproc.terminate()
8299

pyccc/files/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@
88
from .stringcontainer import *
99
from .localfiles import *
1010
from .remotefiles import *
11+
from .directory import *

pyccc/files/directory.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Copyright 2016 Autodesk Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
16+
class LocalDirectoryReference(object):
17+
""" This is a reference to a specific directory on the local filesystem.
18+
19+
This allows entire directories to be staged into the dockerfile as input
20+
"""
21+
def __init__(self, localpath):
22+
self.localpath = localpath
23+
24+
def put(self, destination):
25+
import shutil
26+
shutil.copytree(self.localpath, destination)

pyccc/tests/data/a

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
a

pyccc/tests/data/b

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
b

pyccc/tests/test_job_types.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
PYVERSION = '%s.%s' % (sys.version_info.major, sys.version_info.minor)
1212
PYIMAGE = 'python:%s-slim' % PYVERSION
13+
THISDIR = os.path.dirname(__file__)
1314

1415

1516
########################
@@ -421,3 +422,37 @@ def test_clean_working_dir(fixture, request):
421422
job = engine.launch(image='alpine', command='ls')
422423
job.wait()
423424
assert job.stdout.strip() == ''
425+
426+
427+
class no_context(): # context manager that does nothing that can be used conditionaly
428+
def __enter__(self):
429+
return None
430+
def __exit__(self, exc_type, exc_value, traceback):
431+
return False
432+
433+
434+
@pytest.mark.parametrize('fixture', fixture_types['engine'])
435+
def test_abspath_input_files(fixture, request):
436+
engine = request.getfuncargvalue(fixture)
437+
with pytest.raises(ValueError) if isinstance(engine, pyccc.Subprocess) else no_context():
438+
# this is OK with docker but should fail with a subprocess
439+
job = engine.launch(image='alpine', command='cat /opt/a',
440+
inputs={'/opt/a': pyccc.LocalFile(os.path.join(THISDIR, 'data', 'a'))})
441+
if not isinstance(engine, pyccc.Subprocess):
442+
job.wait()
443+
assert job.exitcode == 0
444+
assert job.stdout.strip() == 'a'
445+
446+
447+
@pytest.mark.parametrize('fixture', fixture_types['engine'])
448+
def test_directory_input(fixture, request):
449+
engine = request.getfuncargvalue(fixture)
450+
451+
# this is OK with docker but should fail with a subprocess
452+
job = engine.launch(image='alpine', command='cat data/a data/b',
453+
inputs={'data':
454+
pyccc.LocalDirectoryReference(os.path.join(THISDIR, 'data'))})
455+
job.wait()
456+
assert job.exitcode == 0
457+
assert job.stdout.strip() == 'a\nb'
458+

0 commit comments

Comments
 (0)