diff --git a/compose/config/config.py b/compose/config/config.py index ea122bc422d..f208776e4b6 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -15,6 +15,8 @@ from .validation import validate_top_level_object from compose.cli.utils import find_candidates_in_parent_dirs +from docker.utils import is_remote + DOCKER_CONFIG_KEYS = [ 'cap_add', @@ -428,16 +430,17 @@ def resolve_volume_path(volume, working_dir, service_name): return container_path -def resolve_build_path(build_path, working_dir=None): - if working_dir is None: - raise Exception("No working_dir passed to resolve_build_path") - return expand_path(working_dir, build_path) +def resolve_build_path(build_path, working_dir): + if is_remote(build_path): + return build_path + else: + return expand_path(working_dir, build_path) def validate_paths(service_dict): if 'build' in service_dict: build_path = service_dict['build'] - if not os.path.exists(build_path) or not os.access(build_path, os.R_OK): + if not is_remote(build_path) and (not os.path.exists(build_path) or not os.access(build_path, os.R_OK)): raise ConfigurationError("build path %s either does not exist or is not accessible." % build_path) diff --git a/compose/service.py b/compose/service.py index a15ee1b9afa..6471b041c95 100644 --- a/compose/service.py +++ b/compose/service.py @@ -10,6 +10,8 @@ import six from docker.errors import APIError +from docker.utils import ContextError +from docker.utils import create_context_from_path from docker.utils import create_host_config from docker.utils import LogConfig from docker.utils.ports import build_port_bindings @@ -707,7 +709,13 @@ def _get_container_host_config(self, override_options, one_off=False): ) def build(self, no_cache=False): - log.info('Building %s...' % self.name) + build_ctx = None + context_path = six.binary_type(self.options['build']) + dockerfile = self.options.get('dockerfile', 'Dockerfile') + try: + build_ctx = create_context_from_path(context_path, dockerfile=dockerfile) + except ContextError as ce: + raise BuildError(self, ce.message) path = self.options['build'] # python2 os.path() doesn't support unicode, so we need to encode it to @@ -716,13 +724,13 @@ def build(self, no_cache=False): path = path.encode('utf8') build_output = self.client.build( - path=path, + build_ctx.path, tag=self.image_name, stream=True, rm=True, pull=False, nocache=no_cache, - dockerfile=self.options.get('dockerfile', None), + **build_ctx.job_params ) try: diff --git a/docs/yml.md b/docs/yml.md index 6fb31a7db96..bd047849b3e 100644 --- a/docs/yml.md +++ b/docs/yml.md @@ -35,13 +35,48 @@ pull if it doesn't exist locally. ### build -Path to a directory containing a Dockerfile. When the value supplied is a -relative path, it is interpreted as relative to the location of the yml file -itself. This directory is also the build context that is sent to the Docker daemon. +Path to a Dockerfile or to a context containing a Dockerfile. When the +value supplied is a relative filesystem path, it is interpreted as relative +to the location of the config file. + +The value supplied to this parameter can have any of the forms accepted by +the docker remote api `/build` endpoint: + +* An http(s) or git URL. The resource at the other end of the URL can be +either a compressed tarball or raw Dockerfile in the case of http(s) or a +souce code repository having a Dockerfile at the root in the case of git. +To be interpreted as a remote URL the value must have one of the following +prefixes: `http://`, `https://`, `git@`, `git://` or `github.com`. + + + Examples: + + A git repo: `build: https://github.com//.git`. The +docker daemon clones the repo and builds from the resultant git workdir. + + A remote tarball: `build: http://remote/context.tar.bz2` The docker +daemon downloads the context, unpacks it, and builds the image. + +* The build context can also be given as a path to a single Dockerfile. +Compose will create a tar archive with that Dockerfile inside and ship it +to the docker daemon to build: + + + `build: /path/to/Dockerfile` + +* You can also build from generated tarball contexts. The tar archive is +sent 'as is' to the daemon - Compose does not validate its contents, it +just checks to see if the supplied value points to a readable tar file. +The docker daemon supports archives compressed in the following formats: +`identity` (no compression), `gzip`, `xz` and `bzip2`. + + + `build: /path/to/localcontext.tar.gz` + +* Finally, `build` can be a path to a local directory containing a Dockerfile +in it. The contents of the directory are packaged in a uncompressed tar +archive and sent to the daemon. If there's a `.dockerignore` file inside +the directory, Compose will filter the directory accordingly. + + + `build: /path/to/localdir` Compose will build and tag it with a generated name, and use that image thereafter. - build: /path/to/build/dir ### dockerfile diff --git a/requirements.txt b/requirements.txt index e93db7b361d..1cba7715d46 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ PyYAML==3.10 -docker-py==1.3.1 dockerpty==0.3.4 docopt==0.6.1 +git+https://github.com/moysesb/docker-py.git@distinct_build_calls#egg=docker-py-1.4.0-dev jsonschema==2.5.1 requests==2.6.1 six==1.7.3 diff --git a/setup.py b/setup.py index 33335047bca..9439f2b93e0 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ def find_version(*file_paths): 'requests >= 2.6.1, < 2.7', 'texttable >= 0.8.1, < 0.9', 'websocket-client >= 0.32.0, < 1.0', - 'docker-py >= 1.3.1, < 1.4', + 'docker-py >= 1.3.1dev0, < 1.4', 'dockerpty >= 0.3.4, < 0.4', 'six >= 1.3.0, < 2', 'jsonschema >= 2.5.1, < 3', diff --git a/tests/fixtures/build-ctx/ctx.tar.xz b/tests/fixtures/build-ctx/ctx.tar.xz new file mode 100644 index 00000000000..5bfc52586b2 Binary files /dev/null and b/tests/fixtures/build-ctx/ctx.tar.xz differ diff --git a/tests/unit/config_test.py b/tests/unit/config_test.py index ccd5b57bf79..fdd4b73dfb8 100644 --- a/tests/unit/config_test.py +++ b/tests/unit/config_test.py @@ -973,6 +973,14 @@ def test_from_file(self): service_dict = load_from_filename('tests/fixtures/build-path/docker-compose.yml') self.assertEquals(service_dict, [{'name': 'foo', 'build': self.abs_context_path}]) + def test_from_tarball_context(self): + service_dict = config.make_service_dict( + 'tarball', + {'build': '../build-ctx/ctx.tar.xz'}, + working_dir='tests/fixtures/build-path' + ) + self.assertEquals(service_dict['build'], os.path.join(self.abs_context_path, 'ctx.tar.xz')) + class GetConfigPathTestCase(unittest.TestCase):