diff --git a/dockermake/cli.py b/dockermake/cli.py index 5d4ec64..09a2498 100644 --- a/dockermake/cli.py +++ b/dockermake/cli.py @@ -31,6 +31,9 @@ def make_arg_parser(): help="Print or build all images (or those specified by _ALL_)") bo.add_argument('-l', '--list', action='store_true', help='List all available targets in the file, then exit.') + bo.add_argument('--build-arg', action='append', + help="Set build-time variables (used the same way as docker build --build-arg)" + ', e.g., `... --build-arg VAR1=val1 --build-arg VAR2=val2`') bo.add_argument('--requires', nargs="*", help='Build a special image from these requirements. Requires --name') bo.add_argument('--name', type=str, diff --git a/dockermake/errors.py b/dockermake/errors.py index c52f582..6d9d2e3 100644 --- a/dockermake/errors.py +++ b/dockermake/errors.py @@ -74,6 +74,10 @@ class MultipleIgnoreError(UserException): CODE = 51 +class CLIError(UserException): + CODE = 52 + + class BuildError(Exception): CODE = 200 diff --git a/dockermake/imagedefs.py b/dockermake/imagedefs.py index 1326cf6..7b0ee22 100644 --- a/dockermake/imagedefs.py +++ b/dockermake/imagedefs.py @@ -122,7 +122,7 @@ def _check_yaml_and_paths(ymlfilepath, yamldefs): (key, imagename, relpath)) def generate_build(self, image, targetname, rebuilds=None, cache_repo='', cache_tag='', - **kwargs): + buildargs=None, **kwargs): """ Separate the build into a series of one or more intermediate steps. Each specified build directory gets its own step @@ -133,6 +133,7 @@ def generate_build(self, image, targetname, rebuilds=None, cache_repo='', cache_ rebuilds (List[str]): list of image layers to rebuild (i.e., without docker's cache) cache_repo (str): repository to get images for caches in builds cache_tag (str): tags to use from repository for caches in builds + buildargs (dict): build-time dockerfile arugments **kwargs (dict): extra keyword arguments for the BuildTarget object """ from_image = self.get_external_base_image(image) @@ -163,7 +164,8 @@ def generate_build(self, image, targetname, rebuilds=None, cache_repo='', cache_ dockermake.step.BuildStep( base_name, base_image, self.ymldefs[base_name], buildname, bust_cache=base_name in rebuilds, - build_first=build_first, cache_from=cache_from)) + build_first=build_first, cache_from=cache_from, + buildargs=buildargs)) base_image = buildname build_first = None diff --git a/dockermake/step.py b/dockermake/step.py index ca385df..31d703d 100644 --- a/dockermake/step.py +++ b/dockermake/step.py @@ -36,10 +36,12 @@ class BuildStep(object): buildname (str): what to call this image, once built bust_cache(bool): never use docker cache for this build step cache_from (str or list): use this(these) image(s) to resolve build cache + buildargs (dict): build-time "buildargs" for dockerfiles """ def __init__(self, imagename, baseimage, img_def, buildname, - build_first=None, bust_cache=False, cache_from=None): + build_first=None, bust_cache=False, cache_from=None, + buildargs=None): self.imagename = imagename self.baseimage = baseimage self.img_def = img_def @@ -50,6 +52,7 @@ def __init__(self, imagename, baseimage, img_def, buildname, self.build_first = build_first self.custom_exclude = self._get_ignorefile(img_def) self.ignoredefs_file = img_def.get('ignorefile', img_def['_sourcefile']) + self.buildargs = buildargs if cache_from and isinstance(cache_from, str): self.cache_from = [cache_from] else: @@ -99,24 +102,25 @@ def build(self, client, pull=False, usecache=True): dockerfile = u'\n'.join(self.dockerfile_lines) - build_args = dict(tag=self.buildname, - pull=pull, - nocache=not usecache, - decode=True, rm=True) + kwargs = dict(tag=self.buildname, + pull=pull, + nocache=not usecache, + decode=True, rm=True, + buildargs=self.buildargs) if usecache: - utils.set_build_cachefrom(self.cache_from, build_args, client) + utils.set_build_cachefrom(self.cache_from, kwargs, client) if self.build_dir is not None: tempdir = self.write_dockerfile(dockerfile) context_path = os.path.abspath(os.path.expanduser(self.build_dir)) - build_args.update(fileobj=None, + kwargs.update(fileobj=None, dockerfile=os.path.join(DOCKER_TMPDIR, 'Dockerfile')) print(colored(' Build context:', 'blue'), colored(os.path.relpath(context_path), 'blue', attrs=['bold'])) if not self.custom_exclude: - build_args.update(path=context_path) + kwargs.update(path=context_path) else: print(colored(' Custom .dockerignore from:','blue'), colored(os.path.relpath(self.ignoredefs_file), 'blue', attrs=['bold'])) @@ -124,27 +128,27 @@ def build(self, client, pull=False, usecache=True): exclude=self.custom_exclude, dockerfile=os.path.join(DOCKER_TMPDIR, 'Dockerfile'), gzip=False) - build_args.update(fileobj=context, + kwargs.update(fileobj=context, custom_context=True) else: if sys.version_info.major == 2: - build_args.update(fileobj=StringIO(dockerfile), + kwargs.update(fileobj=StringIO(dockerfile), path=None, dockerfile=None) else: - build_args.update(fileobj=BytesIO(dockerfile.encode('utf-8')), + kwargs.update(fileobj=BytesIO(dockerfile.encode('utf-8')), path=None, dockerfile=None) tempdir = None # start the build - stream = client.api.build(**build_args) + stream = client.api.build(**kwargs) try: utils.stream_docker_logs(stream, self.buildname) except (ValueError, docker.errors.APIError) as e: - raise errors.BuildError(dockerfile, str(e), build_args) + raise errors.BuildError(dockerfile, str(e), kwargs) # remove the temporary dockerfile if tempdir is not None: diff --git a/dockermake/utils.py b/dockermake/utils.py index 0088174..b4895ef 100644 --- a/dockermake/utils.py +++ b/dockermake/utils.py @@ -19,7 +19,7 @@ import yaml import docker.errors -from termcolor import cprint +from termcolor import cprint, colored from . import errors @@ -81,7 +81,7 @@ def get_build_targets(args, defs): elif args.all: # build all targets in the file assert len(args.TARGETS) == 0, "Pass either a list of targets or `--all`, not both" - if defs.all_targets is not None: + if defs.all_targets: targets = defs.all_targets else: targets = list(defs.ymldefs.keys()) @@ -106,15 +106,20 @@ def build_targets(args, defs, targets): password=args.registry_token, registry=registry, reauth=True) - print("\nREGISTRY LOGIN SUCCESS:",registry) + print("\nREGISTRY LOGIN SUCCESS:", registry) + if args.build_arg: + buildargs = _make_buildargs(args.build_arg) + else: + buildargs = None built, warnings = [], [] builders = [defs.generate_build(t, generate_name(t, args.repository, args.tag), rebuilds=args.bust_cache, cache_repo=args.cache_repo, cache_tag=args.cache_tag, - keepbuildtags=args.keep_build_tags) + keepbuildtags=args.keep_build_tags, + buildargs=buildargs) for t in targets] for b in builders: b.build(client, @@ -139,6 +144,22 @@ def build_targets(args, defs, targets): return built, warnings +def _make_buildargs(build_args): + if build_args: + cprint('Build arguments:', attrs=['bold']) + argdict = {} + for buildarg in build_args: + try: + split = buildarg.index('=') + except ValueError: + raise errors.CLIError('Buildarg options must be passed as --build-arg NAME=VALUE') + argname = buildarg[:split] + argval = buildarg[split+1:] + argdict[argname] = argval + print(' -', colored(argname, 'yellow'), '=', colored(argval, 'blue')) + return argdict + + def push(client, name): success = False warnings = [] diff --git a/test/data/build-args.yml b/test/data/build-args.yml new file mode 100644 index 0000000..cc6a6bc --- /dev/null +++ b/test/data/build-args.yml @@ -0,0 +1,5 @@ +target-buildargs: + FROM: continuumio/miniconda3 + build: | + ARG FILENAME + RUN echo 'hello world' > ${FILENAME} diff --git a/test/test_features.py b/test/test_features.py index e97c168..888bf4e 100644 --- a/test/test_features.py +++ b/test/test_features.py @@ -114,3 +114,9 @@ def test_keep_build_tags(twostep, docker_client): run_docker_make('-f data/twostep.yml target-twostep --keep-build-tags') docker_client.images.get('dmkbuild_target-twostep_1') docker_client.images.get('dmkbuild_target-twostep_2') + + +buildargs = creates_images('target-buildargs') +def test_build_args(buildargs): + run_docker_make('-f data/build-args.yml --build-arg FILENAME=hello-world.txt target-buildargs') + assert_file_content('target-buildargs', 'hello-world.txt', 'hello world') \ No newline at end of file