From 24234d93658e2f0d2d8727a97a01b3fcb11e94c4 Mon Sep 17 00:00:00 2001 From: "Kacper Kowalik (Xarthisius)" Date: Mon, 20 May 2019 10:53:58 -0500 Subject: [PATCH] Allow absolute paths in build_script_files. Fixes #673 --- repo2docker/buildpacks/base.py | 51 ++++++++++++++++++++++++++--- tests/unit/test_external_scripts.py | 42 ++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 5 deletions(-) create mode 100644 tests/unit/test_external_scripts.py diff --git a/repo2docker/buildpacks/base.py b/repo2docker/buildpacks/base.py index 5f3e4305..0ac71992 100644 --- a/repo2docker/buildpacks/base.py +++ b/repo2docker/buildpacks/base.py @@ -5,8 +5,10 @@ import io import os import re import logging -import docker +import string import sys +import hashlib +import escapism import xml.etree.ElementTree as ET from traitlets import Dict @@ -509,6 +511,17 @@ class BuildPack: "RUN {}".format(textwrap.dedent(script.strip("\n"))) ) + # Based on a physical location of a build script on the host, + # create a mapping between: + # 1. Location of a build script in a Docker build context + # ('assemble_files/-<6-chars-of-its-hash>') + # 2. Location of the aforemention script in the Docker image + # Base template basically does: COPY <1.> <2.> + build_script_files = { + self.generate_build_context_filename(k)[0]: v + for k, v in self.get_build_script_files().items() + } + return t.render( packages=sorted(self.get_packages()), path=self.get_path(), @@ -518,13 +531,42 @@ class BuildPack: build_script_directives=build_script_directives, pre_assemble_script_directives=pre_assemble_script_directives, assemble_script_directives=assemble_script_directives, - build_script_files=self.get_build_script_files(), + build_script_files=build_script_files, base_packages=sorted(self.get_base_packages()), post_build_scripts=self.get_post_build_scripts(), start_script=self.get_start_script(), appendix=self.appendix, ) + @staticmethod + def generate_build_context_filename(src_path, hash_length=6): + """ + Generate a filename for a file injected into the Docker build context. + + In case the src_path is relative, it's assumed it's relative to directory of + this __file__. Returns the resulting filename and an absolute path to the source + file on host. + """ + if not os.path.isabs(src_path): + src_parts = src_path.split("/") + src_path = os.path.join(os.path.dirname(__file__), *src_parts) + + src_path_hash = hashlib.sha256(src_path.encode("utf-8")).hexdigest() + safe_chars = set(string.ascii_letters + string.digits) + + def escape(s): + return escapism.escape(s, safe=safe_chars, escape_char="-") + + src_path_slug = escape(src_path) + filename = "build_script_files/{name}-{hash}" + return ( + filename.format( + name=src_path_slug[: 255 - hash_length - 20], + hash=src_path_hash[:hash_length], + ).lower(), + src_path, + ) + def build( self, client, @@ -554,9 +596,8 @@ class BuildPack: return tar for src in sorted(self.get_build_script_files()): - src_parts = src.split("/") - src_path = os.path.join(os.path.dirname(__file__), *src_parts) - tar.add(src_path, src, filter=_filter_tar) + dest_path, src_path = self.generate_build_context_filename(src) + tar.add(src_path, dest_path, filter=_filter_tar) tar.add(ENTRYPOINT_FILE, "repo2docker-entrypoint", filter=_filter_tar) diff --git a/tests/unit/test_external_scripts.py b/tests/unit/test_external_scripts.py new file mode 100644 index 00000000..1265d3ac --- /dev/null +++ b/tests/unit/test_external_scripts.py @@ -0,0 +1,42 @@ +"""Test if assemble scripts from outside of r2d repo are accepted.""" +import time +from repo2docker.app import Repo2Docker +from repo2docker.buildpacks import PythonBuildPack + + +def test_Repo2Docker_external_build_scripts(tmpdir): + tempfile = tmpdir.join("absolute-script") + tempfile.write("Hello World of Absolute Paths!") + + class MockBuildPack(PythonBuildPack): + def detect(self): + return True + + def get_build_script_files(self): + files = {str(tempfile): "/tmp/my_extra_script"} + files.update(super().get_build_script_files()) + return files + + app = Repo2Docker(repo=str(tmpdir)) + app.buildpacks = [MockBuildPack] + app.initialize() + app.build() + container = app.start_container() + + # give the container a chance to start + tic = 180 + while container.status != "running" or tic < 0: + time.sleep(1) + tic -= 1 + + assert container.status == "running" + + try: + status, output = container.exec_run(["sh", "-c", "cat /tmp/my_extra_script"]) + assert status == 0 + assert output.decode("utf-8") == "Hello World of Absolute Paths!" + finally: + container.stop(timeout=1) + container.reload() + assert container.status == "exited", container.status + container.remove()