Merge pull request #2653 from shin-/Runscope-build-args

Support for build arguments
This commit is contained in:
Aanand Prasad 2016-01-14 23:13:54 +00:00
commit 47e53b49c2
9 changed files with 317 additions and 88 deletions

View file

@ -2,6 +2,7 @@ from __future__ import absolute_import
from __future__ import unicode_literals
import codecs
import functools
import logging
import operator
import os
@ -462,6 +463,11 @@ def resolve_environment(service_dict):
return dict(resolve_env_var(k, v) for k, v in six.iteritems(env))
def resolve_build_args(build):
args = parse_build_arguments(build.get('args'))
return dict(resolve_env_var(k, v) for k, v in six.iteritems(args))
def validate_extended_service_dict(service_dict, filename, service):
error_prefix = "Cannot extend service '%s' in %s:" % (service, filename)
@ -499,12 +505,16 @@ def process_service(service_config):
for path in to_list(service_dict['env_file'])
]
if 'build' in service_dict:
if isinstance(service_dict['build'], six.string_types):
service_dict['build'] = resolve_build_path(working_dir, service_dict['build'])
elif isinstance(service_dict['build'], dict) and 'context' in service_dict['build']:
path = service_dict['build']['context']
service_dict['build']['context'] = resolve_build_path(working_dir, path)
if 'volumes' in service_dict and service_dict.get('volume_driver') is None:
service_dict['volumes'] = resolve_volume_paths(working_dir, service_dict)
if 'build' in service_dict:
service_dict['build'] = resolve_build_path(working_dir, service_dict['build'])
if 'labels' in service_dict:
service_dict['labels'] = parse_labels(service_dict['labels'])
@ -542,6 +552,8 @@ def finalize_service(service_config, service_names, version):
if 'restart' in service_dict:
service_dict['restart'] = parse_restart_spec(service_dict['restart'])
normalize_build(service_dict, service_config.working_dir)
return normalize_v1_service_format(service_dict)
@ -556,6 +568,12 @@ def normalize_v1_service_format(service_dict):
service_dict['logging']['options'] = service_dict['log_opt']
del service_dict['log_opt']
if 'dockerfile' in service_dict:
service_dict['build'] = service_dict.get('build', {})
service_dict['build'].update({
'dockerfile': service_dict.pop('dockerfile')
})
return service_dict
@ -606,10 +624,31 @@ def merge_service_dicts(base, override, version):
if version == 1:
legacy_v1_merge_image_or_build(d, base, override)
else:
merge_build(d, base, override)
return d
def merge_build(output, base, override):
build = {}
if 'build' in base:
if isinstance(base['build'], six.string_types):
build['context'] = base['build']
else:
build.update(base['build'])
if 'build' in override:
if isinstance(override['build'], six.string_types):
build['context'] = override['build']
else:
build.update(override['build'])
if build:
output['build'] = build
def legacy_v1_merge_image_or_build(output, base, override):
output.pop('image', None)
output.pop('build', None)
@ -629,22 +668,6 @@ def merge_environment(base, override):
return env
def parse_environment(environment):
if not environment:
return {}
if isinstance(environment, list):
return dict(split_env(e) for e in environment)
if isinstance(environment, dict):
return dict(environment)
raise ConfigurationError(
"environment \"%s\" must be a list or mapping," %
environment
)
def split_env(env):
if isinstance(env, six.binary_type):
env = env.decode('utf-8', 'replace')
@ -654,6 +677,42 @@ def split_env(env):
return env, None
def split_label(label):
if '=' in label:
return label.split('=', 1)
else:
return label, ''
def parse_dict_or_list(split_func, type_name, arguments):
if not arguments:
return {}
if isinstance(arguments, list):
return dict(split_func(e) for e in arguments)
if isinstance(arguments, dict):
return dict(arguments)
raise ConfigurationError(
"%s \"%s\" must be a list or mapping," %
(type_name, arguments)
)
parse_build_arguments = functools.partial(parse_dict_or_list, split_env, 'build arguments')
parse_environment = functools.partial(parse_dict_or_list, split_env, 'environment')
parse_labels = functools.partial(parse_dict_or_list, split_label, 'labels')
def parse_ulimits(ulimits):
if not ulimits:
return {}
if isinstance(ulimits, dict):
return dict(ulimits)
def resolve_env_var(key, val):
if val is not None:
return key, val
@ -697,6 +756,21 @@ def resolve_volume_path(working_dir, volume):
return container_path
def normalize_build(service_dict, working_dir):
if 'build' in service_dict:
build = {}
# Shortcut where specifying a string is treated as the build context
if isinstance(service_dict['build'], six.string_types):
build['context'] = service_dict.pop('build')
else:
build.update(service_dict['build'])
if 'args' in build:
build['args'] = resolve_build_args(build)
service_dict['build'] = build
def resolve_build_path(working_dir, build_path):
if is_url(build_path):
return build_path
@ -709,7 +783,13 @@ def is_url(build_path):
def validate_paths(service_dict):
if 'build' in service_dict:
build_path = service_dict['build']
build = service_dict.get('build', {})
if isinstance(build, six.string_types):
build_path = build
elif isinstance(build, dict) and 'context' in build:
build_path = build['context']
if (
not is_url(build_path) and
(not os.path.exists(build_path) or not os.access(build_path, os.R_OK))
@ -764,32 +844,6 @@ def join_path_mapping(pair):
return ":".join((host, container))
def parse_labels(labels):
if not labels:
return {}
if isinstance(labels, list):
return dict(split_label(e) for e in labels)
if isinstance(labels, dict):
return dict(labels)
def split_label(label):
if '=' in label:
return label.split('=', 1)
else:
return label, ''
def parse_ulimits(ulimits):
if not ulimits:
return {}
if isinstance(ulimits, dict):
return dict(ulimits)
def expand_path(working_dir, path):
return os.path.abspath(os.path.join(working_dir, os.path.expanduser(path)))

View file

@ -15,7 +15,20 @@
"type": "object",
"properties": {
"build": {"type": "string"},
"build": {
"oneOf": [
{"type": "string"},
{
"type": "object",
"properties": {
"context": {"type": "string"},
"dockerfile": {"type": "string"},
"args": {"$ref": "#/definitions/list_or_dict"}
},
"additionalProperties": false
}
]
},
"cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"cgroup_parent": {"type": "string"},
@ -32,7 +45,6 @@
"devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"dns": {"$ref": "#/definitions/string_or_list"},
"dns_search": {"$ref": "#/definitions/string_or_list"},
"dockerfile": {"type": "string"},
"domainname": {"type": "string"},
"entrypoint": {
"oneOf": [

View file

@ -150,18 +150,29 @@ def handle_error_for_schema_with_id(error, service_name):
VALID_NAME_CHARS)
if schema_id == '#/definitions/constraints':
# Build context could in 'build' or 'build.context' and dockerfile could be
# in 'dockerfile' or 'build.dockerfile'
context = False
dockerfile = 'dockerfile' in error.instance
if 'build' in error.instance:
if isinstance(error.instance['build'], six.string_types):
context = True
else:
context = 'context' in error.instance['build']
dockerfile = dockerfile or 'dockerfile' in error.instance['build']
# TODO: only applies to v1
if 'image' in error.instance and 'build' in error.instance:
if 'image' in error.instance and context:
return (
"Service '{}' has both an image and build path specified. "
"A service can either be built to image or use an existing "
"image, not both.".format(service_name))
if 'image' not in error.instance and 'build' not in error.instance:
if 'image' not in error.instance and not context:
return (
"Service '{}' has neither an image nor a build path "
"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:
if 'image' in error.instance and dockerfile:
return (
"Service '{}' has both an image and alternate Dockerfile. "
"A service can either be built to image or use an existing "

View file

@ -638,7 +638,8 @@ class Service(object):
def build(self, no_cache=False, pull=False, force_rm=False):
log.info('Building %s' % self.name)
path = self.options['build']
build_opts = self.options.get('build', {})
path = build_opts.get('context')
# python2 os.path() doesn't support unicode, so we need to encode it to
# a byte string
if not six.PY3:
@ -652,7 +653,8 @@ class Service(object):
forcerm=force_rm,
pull=pull,
nocache=no_cache,
dockerfile=self.options.get('dockerfile', None),
dockerfile=build_opts.get('dockerfile', None),
buildargs=build_opts.get('args', None),
)
try: