def HEALTHCHECK(interval=None, timeout=None, start_period=None, retries=None, cmd=None): """Create a Dockerfile HEALTHCHECK instruction. Args: interval: Optional string containing the duration to wait both prior to the first heathcheck invocation after container start and the interval to wait after a successful healthcheck for applying the next healthcheck. timeout: Optional string containing the duration of time afterwhich a running healthcheck will expire and be considered as failed. start_period: Optional string containing a duration of time to consider as a start-up phase for a container, a phase during which failed healthchecks don't count towards the maximum retries. retries: Optional string containing the number of retries to attempt before consecutive healthcheck failures result in an 'unhealthy' container state. cmd: Optional. Either a single string containing one command or else a list or tuple containing an executable in the first element and optional parameters in the remaining elements. Returns: An instance of the HEALTHCHECK namedtuple. Raises: DockerphileError: raised when arguments are misspecified. """ parameters = { 'interval': interval, 'timeout': timeout, 'start_period': start_period, 'retries': retries } for param, value in parameters.items(): if not isinstance(value, (str, type(None))): raise DockerphileError( 'HEALTHCHECK parameter %s requires string, not %s' % (param, type(value))) if not isinstance(cmd, (str, list, tuple, type(None))): raise DockerphileError( ('HEALTHCHECK "CMD" option must be a string or a list/tuple of ' 'strings, not %s') % type(cmd)) if isinstance(cmd, (list, tuple)) and not cmd: raise DockerphileError( 'HEALTHCHECK "CMD" exec format must have at least 1 executable.') return HEALTHCHECK_t(interval=interval, timeout=timeout, start_period=start_period, retries=retries, cmd=cmd)
def COPY(resources, from_=None): """Create a Dockerfile COPY instruction. Args: resources: A list or tuple of N strings (N >= 2) such that the first N - 1 are treated as COPY source URIs and the Nth is treated as the destination URI in the image being built. All URI strings will be enclosed with double-quote characters in the rendered Dockerfile. from_: Optional string naming a Docker image or index to specify a source image from which to copy when using multi-stage builds. Returns: An instance of the COPY namedtuple. Raises: DockerphileError: raised when `resources` or `from` arguments are not specified in a compatible way. """ msg = '' if not isinstance(resources, (list, tuple)): msg = 'COPY instruction requires list or tuple, not %s' % ( type(resources) ) elif len(resources) < 2: msg = 'COPY instruction must have at least 1 src and 1 dest URI.' elif not isinstance(from_, (str, type(None))): msg = 'COPY --from option must be a string, not %s' % type(from_) if msg: raise DockerphileError(msg) return COPY_t(resources=resources, from_=from_)
def run_block(self, form='shell'): """Syntactiv sugar for RUN block.""" if form not in {'exec', 'shell'}: raise DockerphileError( 'Invalid format specifier for RUN instruction %s' % form ) return RunBlock(self, form=form)
def FROM(base_image, as_=None): """Create a Dockerfile FROM instruction. Args: base_image: A string naming a base Docker image. The string can contain references to pre-FROM ARG variables in accordance with Dockerfile support for ARG variables appearing in FROM instructions. as_: Optional string declaring a name for the image for later reference during a multi-stage build. Returns: An instance of the FROM namedtuple. Raises: DockerphileError: raised when `base_image` or `as_` arguments are misspecified. """ msg = '' if not isinstance(base_image, str): msg = 'FROM parameter `base_image` requires string, not %s' % ( type(base_image)) elif not isinstance(as_, (str, type(None))): msg = 'FROM parameter `as_` requires string, not %s' % (type(as_)) if msg: raise DockerphileError(msg) return FROM_t(base_image=base_image, as_=as_)
def ADD(resources): """Create a Dockerfile ADD instruction. Args: resources: A list or tuple of N strings (N >= 2) such that the first N - 1 are treated as ADD source URIs and the Nth is treated as the destination URI in the image being built. All URI strings will be enclosed with double-quote characters in the rendered Dockerfile. Returns: An instance of the ADD namedtuple. Raises: DockerphileError: raised when `resources` argument is not specified in a compatible way. """ msg = '' if not isinstance(resources, (list, tuple)): msg = 'ADD instruction requires list or tuple, not %s' % type( resources) elif len(resources) < 2: msg = 'ADD instruction must have at least 1 src and 1 dest URI.' if msg: raise DockerphileError(msg) return ADD_t(resources=resources)
def VOLUME(volume_specs): """Create a Dockerfile VOLUME instruction. Args: volume_specs: A list or tuple of strings each containing a valid Dockerfile volume specifier. The volume specifiers will be rendered in the JSON array format in resulting Dockerfiles and enclosed in double quotes. Returns: An instance of the VOLUME namedtuple. Raises: DockerphileError: raised when `volume_specs` argument is misspecified. """ msg = '' if not isinstance(volume_specs, (list, tuple)): msg = 'VOLUME instruction requires list or tuple, not %s' % ( type(volume_specs)) elif not volume_specs: msg = 'VOLUME instruction must have at least 1 volume specifier.' if msg: raise DockerphileError(msg) return VOLUME_t(volume_specs=volume_specs)
def ARG(key, default_value=None): """Create a Dockerfile ARG instruction. Args: key: string that names the Dockerfile build ARG. default_value: string supplying the ARG default value (e.g. `key=value`) in the Dockerfile. Returns: An instance of the ARG namedtuple. Raises: DockerphileError: raised for any errors when specifying `key` or `default_value`. """ msg = '' if not isinstance(key, str): msg = 'ARG instruction key requires string, not %s' % type(key) elif not isinstance(default_value, (str, type(None))): msg = 'ARG instruction default_value requires string, not %s' % ( type(default_value)) if msg: raise DockerphileError(msg) return ARG_t(key=key, default_value=default_value)
def run(self, command, form='shell'): """run_t""" if form not in {'exec', 'shell'}: raise DockerphileError( 'Invalid format specifier for RUN instruction %s' % form ) self.sequence.append( structures.RUN(**{("%s_form" % form): command}) )
def entrypoint(self, entrypoint, form='exec'): """entrypoint_t""" if form not in {'exec', 'shell'}: raise DockerphileError( 'Invalid format specifier for ENTRYPOINT instruction %s' % form ) self.sequence.append( structures.ENTRYPOINT(**{("%s_form" % form): entrypoint}) )
def cmd(self, cmd, form='exec'): """cmd_t.""" if form not in {'exec', 'default', 'shell'}: raise DockerphileError( 'Invalid format specifier for CMD instruction %s' % form ) self.sequence.append( structures.CMD(**{("%s_form" % form): cmd}) )
def _check_format(instruction, arg, argname): """Helper to repeat argument validation for an instruction format.""" msg = '' if not isinstance(arg, (list, tuple)): msg = '%s %s requires list or tuple of strings, not %s' % ( instruction, argname, type(arg)) if not len(arg) >= 1: msg = '%s %s requires at least one parameter.' % (instruction, argname) if msg: raise DockerphileError(msg)
def CMD(exec_form=None, default_form=None, shell_form=None): """Create a Dockerfile CMD instruction. Docker CMD instructions have multiple forms that are rendered in different ways in Dockerfiles. This instruction can accept inputs that are compatible with each of the different CMD forms. Exception handling is performed to ensure that only one CMD form is specified in a single call to this CMD instruction, and that required properties of the input format are satisfied. When using either the exec or default forms of CMD inputs, the result is rendered as a JSON array of strings in any Dockerfile. For the shell form, the result is rendered as a series of unquoted literals directly following the CMD instruction. Args: exec_form: The exec form of input to the Dockerfile CMD instruction. This input must be a list or tuple of strings. The first entry specifies an executable and the remaining entries serve as the parameters to that executable. default_form: The form of CMD instruction input that provides default parameters to the ENTRYPOINT of the Docker image. This form must also be a list or tuple or strings, but all entries are expected to be parameters (not executable) that are implicitly passed to the Docker image's ENTRYPOINT executable. shell_form: The form of input to the CMD instruction that implicitly uses a shell context (not the Docker context) to execute an instruction with parameters. This argument must be a list or tuple of strings. The first entry must name an executable and the remaining entries serve as parameters. Returns: An instance of the CMD namedtuple. Raises: DockerphileError: raised for any errors when specifying the CMD form arguments. """ kwargs = { "exec_form": exec_form, "default_form": default_form, "shell_form": shell_form } if sum(v is not None for k, v in kwargs.items()) != 1: raise DockerphileError('Exactly 1 CMD argument format must be used.') for arg_name, arg_value in kwargs.items(): if arg_value is not None: _check_format("CMD", arg_value, arg_name) return CMD_t(**kwargs)
def STOPSIGNAL(signal): """Create a Dockerfile STOPSIGNAL instruction. Args: signal: a string naming a valid signal identifier according to the Dockerfile STOPSIGNAL instruction specification. Returns: An instance of the STOPSIGNAL namedtuple. Raises: DockerphileError: raised for any errors when specifying `signal`. """ msg = '' if not isinstance(signal, str): msg = 'STOPSIGNAL instruction signal requires string, not %s' % ( type(signal)) if msg: raise DockerphileError(msg) return STOPSIGNAL_t(signal=signal)
def WORKDIR(workdir): """Create a Dockerfile WORKDIR instruction. Args: workdir: a string naming a valid path identifier according to the Dockerfile WORKDIR instruction specification. Returns: An instance of the WORKDIR namedtuple. Raises: DockerphileError: raised for any errors when specifying `workdir`. """ msg = '' if not isinstance(workdir, str): msg = 'WORKDIR parameter `workdir` requires string, not %s' % ( type(workdir)) if msg: raise DockerphileError(msg) return WORKDIR_t(workdir=workdir)
def RUN(shell_form=None, exec_form=None): """Create a Dockerfile RUN instruction. Docker RUN instructions have multiple forms that are rendered in different ways in Dockerfiles. This instruction can accept inputs that are compatible with each of the different RUN forms. Exception handling is performed to ensure that only one RUN format is specified in a single call to this RUN instruction, and that required properties of the input format are satisfied. When using the exec form of RUN inputs, the result is rendered as a JSON array of strings in any Dockerfile. For the shell form, the result is rendered as a series of unquoted literals directly following the RUN instruction. Args: shell_form: The form of input to the RUN instruction that implicitly uses a shell context (not the Docker context) to execute an instruction with parameters. This argument must be a list or tuple of strings. exec_form: The exec form of input to the Dockerfile RUN instruction. This input must be a list or tuple of strings. The first entry specifies an executable and the remaining entries serve as the parameters to that executable. Returns: An instance of the RUN namedtuple. Raises: DockerphileError: raised for any errors when specifying the RUN form arguments. """ kwargs = {"exec_form": exec_form, "shell_form": shell_form} if sum(v is not None for k, v in kwargs.items()) != 1: raise DockerphileError('Exactly 1 RUN argument format must be used.') for arg_name, arg_value in kwargs.items(): if arg_value is not None: _check_format("RUN", arg_value, arg_name) return RUN_t(**kwargs)
def EXPOSE(port_specs): """Create a Dockerfile EXPOSE instruction. Args: port_specs: A list or tuple of strings each containing a valid Dockerfile port/protocol specifier. Returns: An instance of the EXPOSE namedtuple. Raises: DockerphileError: raised when `port_specs` argument is misspecified. """ msg = '' if not isinstance(port_specs, (list, tuple)): msg = 'EXPOSE instruction requires list or tuple, not %s' % ( type(port_specs)) elif not port_specs: msg = 'EXPOSE instruction must have at least 1 port specifier.' if msg: raise DockerphileError(msg) return EXPOSE_t(port_specs=port_specs)
def ESCAPE(character): """Create a Dockerfile `# escape` parser directive. Args: character: a single-character string naming a valid character to serve as the escape character according to the `escape` parser directive specification for Dockerfiles. Returns: An instance of the ESCAPE namedtuple. Raises: DockerphileError: raised for any errors when specifying `character`. """ msg = '' if not isinstance(character, str): msg = 'ESCAPE parameter must be a string, not %s' % (type(character)) elif len(character) != 1: msg = 'ESCAPE parameter must contain exactly 1 character.' if msg: raise DockerphileError(msg) return ESCAPE_t(character=character)
def ENV(key, value): """Create a Dockerfile ENV instruction. Args: key: string that names Dockerfile environment variable. value: string containing the environment variable value to set. Returns: An instance of the ENV namedtuple. Raises: DockerphileError: raised for any errors when specifying `key` or `value`. """ msg = '' if not isinstance(key, str): msg = 'ENV instruction key requires string, not %s' % type(key) elif not isinstance(value, str): msg = 'ENV instruction value requires string, not %s' % type(value) if msg: raise DockerphileError(msg) return ENV_t(key=key, value=value)
def LABEL(key, value): """Create a Dockerfile LABEL instruction. Args: key: string that names Dockerfile label key. value: string containing the label value to set. Returns: An instance of the LABEL namedtuple. Raises: DockerphileError: raised for any errors when specifying `key` or `value`. """ msg = '' if not isinstance(key, str): msg = 'LABEL instruction key requires string, not %s' % type(key) elif not isinstance(value, str): msg = 'LABEL instruction value requires string, not %s' % type(value) if msg: raise DockerphileError(msg) return LABEL_t(key=key, value=value)
def USER(user, group=None): """Create a Dockerfile USER instruction. Args: user: string containing username or UID for Dockerfile USER instruction. group: Optional string containing a group name or ID for the user. Returns: An instance of the USER namedtuple. Raises: DockerphileError: raised for any errors when specifying `user` or `group`. """ msg = '' if not isinstance(user, str): msg = 'USER parameter `user` requires string, not %s' % type(user) elif not isinstance(group, (str, type(None))): msg = 'USER parameter `group` requires string, not %s' % type(group) if msg: raise DockerphileError(msg) return USER_t(user=user, group=group)
def SHELL(shell_spec): """Create a Dockerfile SHELL instruction. Args: shell_spec: A list or tuple of strings together containing a JSON / exec formatted shell executable to be set via the Dockerfile SHELL instruction. Returns: An instance of the SHELL namedtuple. Raises: DockerphileError: raised when `shell_spec` argument is misspecified. """ msg = '' if not isinstance(shell_spec, (list, tuple)): msg = 'SHELL instruction requires list or tuple, not %s' % ( type(shell_spec)) elif not shell_spec: msg = 'SHELL instruction parameter must have at least 1 entry.' if msg: raise DockerphileError(msg) return SHELL_t(shell_spec=shell_spec)
def ONBUILD(instruction): """Create a Dockerfile ONBUILD instruction. ONBUILD instructions must be instances of valid dockerphile types that are permitted to serve as an ONBUILD parameter in a Dockerfile. For example, if you seek to trigger a COPY instruction with an ONBUILD trigger, then you must create an instance of `dockerphile.structures.copy_t.COPY_t`, and pass this object as the instruction argument to ONBUILD. Note that FROM_t, COMMENT_t and ONBUILD_t are not valid parameters for ONBUILD, and these result in exceptions. Additionally, since ESCAPE maps to a parser directive, not a instruction, and it must appear in a Dockerfile prior to any instructions, ESCAPE is also considered invalid for ONBUILD. Args: instruction: An instance of a valid dockerphile instruction. FROM and ONBUILD are invalid to appear as the parameter for an ONBUILD instruction, but any other Dockerfile instruction can appear. Since COMMENT and ESCAPE do not refer to triggerable instructions, they are also not part of the valid instruction set for ONBUILD. Returns: An instance of the ONBUILD namedtuple. Raises: DockerphileError: raised if `instruction` is a dockerphile instruction type that is invalid for ONBUILD or if `instruction` is not an instance of a dockerphile instruction. """ msg = '' if not isinstance(instruction, VALID_TYPES): msg = 'ONBUILD instruction parameter received disallowed type %s.' % ( type(instruction)) if msg: raise DockerphileError(msg) return ONBUILD_t(instruction=instruction)
def to_instruction(parsed_instruction): """Map a parsed `dockerfile.Command` to a `dockerphile.structures` type. `dockerfile.Command` contains the original parsed string (with line continuations and all comments removed) and also contains a `value` attribute storing the part of the Dockerfile instruction non-inclusive of Dockerfile keywords. Known limitations include: (1) no parsing of comment lines; (2) no parsing of the `escape` parser directive; and (2) no direct parsing of the optional arguments for the HEALTHCHECK or COPY commands. Args: parsed_instruction: An instance of `dockerfile.Command`. Returns: Either an instance of a `dockerphile.structures` namedtuple representing the parsed command using `dockerphile` types, or a list of such types. Return value is None for any underlying commands that are not supported by `dockerphile` even if parsed correctly, such as `MAINTAINER` (since `dockerphile` requires following Docker best practices to use `LABEL` for a maintainer label. Raises: DockerphileError: raised if incorrect instruction type is provided or if the parsed `dockerfile.Command` object contains invalid Dockerfile syntax. dockerfile.GoParseError: raised if underlying `dockerfile` library Go parser encounters an unhandled parser error. """ cmd, original = parsed_instruction.cmd, parsed_instruction.original uses_json, value = parsed_instruction.json, parsed_instruction.value if cmd == 'add': return structures.ADD(value) if cmd == 'arg': arg_string = value[0] parsed_arg = re.match(arg_default_value, arg_string) if parsed_arg is not None: key, default_value = parsed_arg.groups() return structures.ARG(key, default_value=default_value) else: return structures.ARG(arg_string) if cmd == 'cmd': return structures.CMD(**{ ('exec_form' if uses_json else 'shell_form'): value }) if cmd == 'copy': parsed_from = re.match(copy_from, original) from_ = parsed_from.groups()[0] if parsed_from is not None else None return structures.COPY(value, from_=from_) if cmd == 'entrypoint': return structures.ENTRYPOINT(**{ ('exec_form' if uses_json else 'shell_form'): value }) if cmd == 'env': if not value: raise DockerphileError("ENV command has no key or value.") if len(value) == 2: return structures.ENV(value[0], value[1]) try: result = [structures.ENV(value[k], value[k + 1]) for k in range(len(value))[::2]] except IndexError: raise DockerphileError("Unpaired index when parsing " "%s to list of ENV types." % value) return result if cmd == 'expose': return structures.EXPOSE(value) if cmd == 'from': parsed_from = re.match(from_as, original) if parsed_from is not None: image, as_ = parsed_from.groups() return structures.FROM(image, as_=as_) if not value: raise DockerphileError("FROM instruction requires nonempty base " "image.") return structures.FROM(value[0]) if cmd == 'healthcheck': if value and value[0] != 'CMD': raise DockerphileError("Invalid CMD specifier for HEALTHCHECK: " "%s" % value[0]) options = { 'interval': re.match(healthcheck_interval, original), 'timeout': re.match(healthcheck_timeout, original), 'start_period': re.match(healthcheck_start_period, original), 'retries': re.match(healthcheck_retries, original) } kwargs = { arg_name: (None if arg_match is None else arg_match.groups()[0]) for arg_name, arg_match in options.items() } if len(value) == 2: kwargs['cmd'] = value[1] elif len(value) > 2: kwargs['cmd'] = value[1:] return structures.HEALTHCHECK(**kwargs) if cmd == 'label': if not value: raise DockerphileError("LABEL command has no key or value.") if len(value) == 2: return structures.LABEL(value[0], value[1]) try: result = [structures.LABEL(value[k], value[k + 1]) for k in range(len(value))[::2]] except IndexError: msg = "Unpaired index when parsing %s to list of LABEL types." raise DockerphileError(msg % value) return result if cmd == 'maintainer': return None if cmd == 'onbuild': if parsed_instruction.sub_cmd == 'onbuild': raise DockerphileError("ONBUILD is not allowed as subcommand of " "ONBUILD.") sub_cmd_str = original.replace('ONBUILD', '') parsed_sub_cmd = parse_string(sub_cmd_str)[0] return structures.ONBUILD(to_instruction(parsed_sub_cmd)) if cmd == 'run': return structures.RUN(**{ ('exec_form' if uses_json else 'shell_form'): value }) if cmd == "shell": if not uses_json: raise DockerphileError("SHELL instruction requires used of JSON " "array option format.") if not value: raise DockerphileError("SHELL instruction requires nonempty JSON " "array option format.") return structures.SHELL(value) if cmd == 'stopsignal': if not value: raise DockerphileError("STOPSIGNAL instruction requires nonempty " "value.") return structures.STOPSIGNAL(value[0]) if cmd == 'user': if not value: raise DockerphileError("USER instruction requires nonempty value.") parsed_user_group = re.match(user_group, original) if parsed_user_group is not None: user, group = parsed_user_group.groups() else: user, group = value[0], None return structures.USER(user, group=group) if cmd == 'volume': return structures.VOLUME(value) if cmd == 'workdir': if not value: raise DockerphileError("WORKDIR instruction requires nonempty " "value.") return structures.WORKDIR(value[0]) raise DockerphileError("Unrecognized dockerfile.Command parsed command " "%s" % cmd)
def render_instruction(instruction): """Render a string format of a dockerphile instruction. Args: instruction: A dockerphile instruction type from `dockerphile.structures`. Returns: A string containing the rendered output of the given instruction. Raises: DockerphileError: raised when the input is not a valid dockerphile instruction type. """ if isinstance(instruction, structures.ADD_t): result = "ADD" for resource in instruction.resources: result = result + ' "%s"' % resource return result if isinstance(instruction, structures.ARG_t): result = "ARG %s" % instruction.key if instruction.default_value is not None: result = result + "=%s" % instruction.default_value return result if isinstance(instruction, structures.CMD_t): result = "CMD" if instruction.shell_form is not None: for c in instruction.shell_form: result = result + " %s" % c return result elif instruction.exec_form is not None: commands = json.dumps(instruction.exec_form) else: commands = json.dumps(instruction.default_form) return result + " " + commands if isinstance(instruction, structures.COMMENT_t): return "# %s" % instruction.comment if isinstance(instruction, structures.COPY_t): result = "COPY" if instruction.from_ is not None: result = result + " --from=%s" % instruction.from_ for resource in instruction.resources: result = result + ' "%s"' % resource return result if isinstance(instruction, structures.ENTRYPOINT_t): result = "ENTRYPOINT" if instruction.shell_form is not None: for c in instruction.shell_form: result = result + " %s" % c return result else: commands = json.dumps(instruction.exec_form) return result + " " + commands if isinstance(instruction, structures.ENV_t): return "ENV %s %s" % (instruction.key, instruction.value) if isinstance(instruction, structures.ESCAPE_t): return "# escape=%s" % instruction.character if isinstance(instruction, structures.EXPOSE_t): result = "EXPOSE" for port_spec in instruction.port_specs: result = result + " %s" % port_spec return result if isinstance(instruction, structures.FROM_t): result = "FROM %s" % instruction.base_image if instruction.as_ is not None: result = result + " AS %s" % instruction.as_ return result if isinstance(instruction, structures.HEALTHCHECK_t): result = "HEALTHCHECK" if instruction.interval is not None: result = result + " \\\n --interval=%s" % instruction.interval if instruction.timeout is not None: result = result + " \\\n --timeout=%s" % instruction.timeout if instruction.start_period is not None: result = result + " \\\n --start-period=%s" % ( instruction.start_period ) if instruction.retries is not None: result = result + " \\\n --retries=%s" % instruction.retries if instruction.cmd is not None: result = result + " \\\n CMD" if isinstance(instruction.cmd, str): return result + " %s" % instruction.cmd else: for c in instruction.cmd: result = result + " %s" % c return result if isinstance(instruction, structures.LABEL_t): return "LABEL %s=%s" % (instruction.key, instruction.value) if isinstance(instruction, structures.ONBUILD_t): return "ONBUILD %s" % render_instruction(instruction.instruction) if isinstance(instruction, structures.RUN_t): result = "RUN" if instruction.shell_form is not None: for c in instruction.shell_form: result = result + " %s" % c return result else: commands = json.dumps(instruction.exec_form) return result + " " + commands if isinstance(instruction, structures.SHELL_t): return "SHELL " + json.dumps(instruction.shell_spec) if isinstance(instruction, structures.STOPSIGNAL_t): return "STOPSIGNAL %s" % instruction.signal if isinstance(instruction, structures.USER_t): result = "USER %s" % instruction.user if instruction.group is not None: result = result + ":%s" % instruction.group return result if isinstance(instruction, structures.VOLUME_t): return "VOLUME " + json.dumps(instruction.volume_specs) if isinstance(instruction, structures.WORKDIR_t): return "WORKDIR %s" % instruction.workdir raise DockerphileError( "Unrecognized or invalid instruction type %s" % instruction )