Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 16 additions & 9 deletions compose/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,8 @@ def merge_services(base, override):
return {
name: merge_service_dicts_from_files(
base.get(name, {}),
override.get(name, {}))
override.get(name, {}),
version)
for name in all_service_names
}

Expand Down Expand Up @@ -397,7 +398,10 @@ def resolve_extends(self, extended_config_path, service_dict, service_name):
service_name,
)

return merge_service_dicts(other_service_dict, self.service_config.config)
return merge_service_dicts(
other_service_dict,
self.service_config.config,
self.version)

def get_extended_config_path(self, extends_options):
"""Service we are extending either has a value for 'file' set, which we
Expand Down Expand Up @@ -521,20 +525,20 @@ def normalize_v1_service_format(service_dict):
return service_dict


def merge_service_dicts_from_files(base, override):
def merge_service_dicts_from_files(base, override, version):
"""When merging services from multiple files we need to merge the `extends`
field. This is not handled by `merge_service_dicts()` which is used to
perform the `extends`.
"""
new_service = merge_service_dicts(base, override)
new_service = merge_service_dicts(base, override, version)
if 'extends' in override:
new_service['extends'] = override['extends']
elif 'extends' in base:
new_service['extends'] = base['extends']
return new_service


def merge_service_dicts(base, override):
def merge_service_dicts(base, override, version):
d = {}

def merge_field(field, merge_func, default=None):
Expand All @@ -545,7 +549,6 @@ def merge_field(field, merge_func, default=None):

merge_field('environment', merge_environment)
merge_field('labels', merge_labels)
merge_image_or_build(base, override, d)

for field in ['volumes', 'devices']:
merge_field(field, merge_path_mappings)
Expand All @@ -556,15 +559,19 @@ def merge_field(field, merge_func, default=None):
for field in ['dns', 'dns_search', 'env_file']:
merge_field(field, merge_list_or_string)

already_merged_keys = set(d) | {'image', 'build'}
for field in set(ALLOWED_KEYS) - already_merged_keys:
for field in set(ALLOWED_KEYS) - set(d):
if field in base or field in override:
d[field] = override.get(field, base.get(field))

if version == 1:
legacy_v1_merge_image_or_build(d, base, override)

return d


def merge_image_or_build(base, override, output):
def legacy_v1_merge_image_or_build(output, base, override):
output.pop('image', None)
output.pop('build', None)
if 'image' in override:
output['image'] = override['image']
elif 'build' in override:
Expand Down
13 changes: 2 additions & 11 deletions compose/config/service_schema_v2.json
Original file line number Diff line number Diff line change
Expand Up @@ -167,17 +167,8 @@
"constraints": {
"id": "#/definitions/constraints",
"anyOf": [
{
"required": ["build"],
"not": {"required": ["image"]}
},
{
"required": ["image"],
"not": {"anyOf": [
{"required": ["build"]},
{"required": ["dockerfile"]}
]}
}
{"required": ["build"]},
{"required": ["image"]}
]
}
}
Expand Down
4 changes: 3 additions & 1 deletion compose/config/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ def handle_error_for_schema_with_id(error, service_name):
VALID_NAME_CHARS)

if schema_id == '#/definitions/constraints':
# TODO: only applies to v1
if 'image' in error.instance and 'build' in error.instance:
return (
"Service '{}' has both an image and build path specified. "
Expand All @@ -159,7 +160,8 @@ def handle_error_for_schema_with_id(error, service_name):
if 'image' not in error.instance and 'build' not in error.instance:
return (
"Service '{}' has neither an image nor a build path "
"specified. Exactly one must be provided.".format(service_name))
"specified. At least one must be provided.".format(service_name))
# TODO: only applies to v1
if 'image' in error.instance and 'dockerfile' in error.instance:
return (
"Service '{}' has both an image and alternate Dockerfile. "
Expand Down
4 changes: 2 additions & 2 deletions compose/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,8 +344,8 @@ def _get_convergence_plans(self, services, strategy):
updated_dependencies = [
name
for name in service.get_dependency_names()
if name in plans
and plans[name].action in ('recreate', 'create')
if name in plans and
plans[name].action in ('recreate', 'create')
]

if updated_dependencies and strategy.allows_recreate:
Expand Down
18 changes: 4 additions & 14 deletions compose/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,10 +275,7 @@ def image(self):

@property
def image_name(self):
if self.can_be_built():
return self.full_name
else:
return self.options['image']
return self.options.get('image', '{s.project}_{s.name}'.format(s=self))

def convergence_plan(self, strategy=ConvergenceStrategy.changed):
containers = self.containers(stopped=True)
Expand Down Expand Up @@ -535,9 +532,9 @@ def _get_container_create_options(
# unqualified hostname and a domainname unless domainname
# was also given explicitly. This matches the behavior of
# the official Docker CLI in that scenario.
if ('hostname' in container_options
and 'domainname' not in container_options
and '.' in container_options['hostname']):
if ('hostname' in container_options and
'domainname' not in container_options and
'.' in container_options['hostname']):
parts = container_options['hostname'].partition('.')
container_options['hostname'] = parts[0]
container_options['domainname'] = parts[2]
Expand Down Expand Up @@ -665,13 +662,6 @@ def build(self, no_cache=False, pull=False, force_rm=False):
def can_be_built(self):
return 'build' in self.options

@property
def full_name(self):
"""
The tag to give to images built for this service.
"""
return '%s_%s' % (self.project, self.name)

def labels(self, one_off=False):
return [
'{0}={1}'.format(LABEL_PROJECT, self.project),
Expand Down
16 changes: 14 additions & 2 deletions tests/integration/service_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,7 @@ def test_build(self):
f.write("FROM busybox\n")

self.create_service('web', build=base_dir).build()
self.assertEqual(len(self.client.images(name='composetest_web')), 1)
assert self.client.inspect_image('composetest_web')

def test_build_non_ascii_filename(self):
base_dir = tempfile.mkdtemp()
Expand All @@ -504,7 +504,19 @@ def test_build_non_ascii_filename(self):
f.write("hello world\n")

self.create_service('web', build=text_type(base_dir)).build()
self.assertEqual(len(self.client.images(name='composetest_web')), 1)
assert self.client.inspect_image('composetest_web')

def test_build_with_image_name(self):
base_dir = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, base_dir)

with open(os.path.join(base_dir, 'Dockerfile'), 'w') as f:
f.write("FROM busybox\n")

image_name = 'examples/composetest:latest'
self.addCleanup(self.client.remove_image, image_name)
self.create_service('web', build=base_dir, image=image_name).build()
assert self.client.inspect_image(image_name)

def test_build_with_git_url(self):
build_url = "https://github.com/dnephin/docker-build-from-url.git"
Expand Down
Loading