Example #1
0
    def from_dict(cls, d: ty.Mapping) -> _Renderer:
        """Instantiate a new renderer from a dictionary of instructions."""
        # raise error if invalid
        _validate_renderer(d)

        pkg_manager = d["pkg_manager"]
        users = d.get("existing_users", None)

        # create new renderer object
        renderer = cls(pkg_manager=pkg_manager, users=users)

        for mapping in d["instructions"]:
            method_or_template = mapping["name"]
            kwds = mapping["kwds"]
            this_instance_method = getattr(renderer, method_or_template, None)
            # Method exists and is something like 'copy', 'env', 'run', etc.
            if this_instance_method is not None:
                try:
                    this_instance_method(**kwds)
                except Exception as e:
                    raise RendererError(
                        f"Error on step '{method_or_template}'. Please see the"
                        " traceback above for details.") from e
            # This is actually a template.
            else:
                try:
                    renderer.add_registered_template(method_or_template,
                                                     **kwds)
                except TemplateError as e:
                    raise RendererError(
                        f"Error on template '{method_or_template}'. Please see above"
                        " for more information.") from e
        return renderer
Example #2
0
def _render_string_from_template(source: str,
                                 template: _BaseInstallationTemplate) -> str:
    """Take a string from a template and render """
    # TODO: we could use a while loop or recursive function to render the template until
    # there are no jinja-specific things. At this point, we support one level of
    # nesting.
    n_renders = 0
    max_renders = 20

    err = (
        "A template included in this renderer raised an error. Please check the"
        " template definition. A required argument might not be included in the"
        " required arguments part of the template. Variables in the template should"
        " start with `self.`.")

    # Render the string again. This is sometimes necessary because some defaults in the
    # template are rendered as {{ self.X }}. These defaults need to be rendered again.

    while (_jinja_env.variable_start_string in source
           and _jinja_env.variable_end_string in source):
        source = source.replace("self.", "template.")
        tmpl = _jinja_env.from_string(source)
        try:
            source = tmpl.render(template=template)
        except jinja2.exceptions.UndefinedError as e:
            raise RendererError(err) from e
        n_renders += 1

        if n_renders > max_renders:
            raise RendererError(
                f"reached maximum rendering iterations ({max_renders}). Templates"
                f" should not nest variables more than {max_renders} times.")
    return source
Example #3
0
    def add_registered_template(self,
                                name: str,
                                method: installation_methods_type = None,
                                **kwds) -> _Renderer:

        # Template was validated at registration time.
        template_dict = _TemplateRegistry.get(name)

        # By default, prefer 'binaries', but use 'source' if 'binaries' is not defined.
        # TODO: should we require user to provide method?
        if method is None:
            method = "binaries" if "binaries" in template_dict else "source"
        if method not in template_dict:
            raise RendererError(
                f"Installation method '{method}' not defined for template '{name}'."
                " Options are '{}'.".format("', '".join(template_dict.keys())))

        binaries_kwds = source_kwds = None
        if method == "binaries":
            binaries_kwds = kwds
        elif method == "source":
            source_kwds = kwds

        template = Template(template=template_dict,
                            binaries_kwds=binaries_kwds,
                            source_kwds=source_kwds)

        self.add_template(template=template, method=method)
        return self
Example #4
0
    def __init__(self,
                 pkg_manager: pkg_managers_type,
                 users: ty.Optional[ty.Set[str]] = None) -> None:
        if pkg_manager not in allowed_pkg_managers:
            raise RendererError(
                "Unknown package manager '{}'. Allowed package managers are"
                " '{}'.".format(pkg_manager,
                                "', '".join(allowed_pkg_managers)))

        self.pkg_manager = pkg_manager
        self._users = {"root"} if users is None else users
        # This keeps track of the current user. This is useful when saving the JSON
        # specification to JSON, because if we are not root, we can change to root,
        # write the file, and return to whichever user we were.
        self._current_user = "******"
        self._instructions: ty.Mapping = {
            "pkg_manager": self.pkg_manager,
            "existing_users": list(self._users),
            "instructions": [],
        }

        # Strings (comments) that indicate the beginning and end of saving the JSON
        # spec to a file. This helps us in testing because sometimes we don't care
        # about the JSON but we do care about everything else. We can remove the
        # content between these two strings.
        self._json_save_start = "# Save specification to JSON."
        self._json_save_end = "# End saving to specification to JSON."
Example #5
0
def _validate_renderer(d):
    """Validate renderer dictionary against JSON schema. Raise exception if invalid."""
    try:
        jsonschema.validate(d, schema=_RENDERER_SCHEMA)
    except jsonschema.exceptions.ValidationError as e:
        raise RendererError(
            f"Invalid renderer dictionary: {e.message}.") from e
Example #6
0
def _install(pkgs: ty.List[str], pkg_manager: str, opts: str = None) -> str:
    if pkg_manager == "apt":
        return _apt_install(pkgs, opts)
    elif pkg_manager == "yum":
        return _yum_install(pkgs, opts)
    # TODO: add debs here?
    else:
        raise RendererError(f"Unknown package manager '{pkg_manager}'.")
Example #7
0
    def from_(self, base_image: str) -> SingularityRenderer:
        if "://" not in base_image:
            bootstrap = "docker"
            image = base_image
        elif base_image.startswith("docker://"):
            bootstrap = "docker"
            image = base_image[9:]
        elif base_image.startswith("library://"):
            bootstrap = "library"
            image = base_image[10:]
        else:
            raise RendererError("Unknown singularity bootstrap agent.")

        self._header = {"bootstrap": bootstrap, "from_": image}
        return self
Example #8
0
def _raise_helper(msg: str) -> ty.NoReturn:
    raise RendererError(msg)
Example #9
0
    def add_template(self, template: Template,
                     method: installation_methods_type) -> _Renderer:
        """Add a template to the renderer.

        Parameters
        ----------
        template : Template
            The template to add. To reference templates by name, use
            `.add_registered_template`.
        method : str
            The method to use to install the software described in the template.
        """

        if not isinstance(template, Template):
            raise RendererError(
                "template must be an instance of 'Template' but got"
                f" '{type(template)}'.")
        if method not in allowed_installation_methods:
            raise RendererError("method must be '{}' but got '{}'.".format(
                "', '".join(sorted(allowed_installation_methods)), method))

        template_method: _BaseInstallationTemplate = getattr(template, method)
        if template_method is None:
            raise RendererError(
                f"template does not have entry for: '{method}'")
        # Validate kwds passed by user to template, and raise an exception if any are
        # invalid.
        template_method.validate_kwds()

        # TODO: print a message if the template has a nonempty `alert` property.
        # If we print to stdout, however, we can cause problems if the user is piping
        # the output to a file or directly to a container build command.

        # If we keep the `self.VAR` syntax of the template, then we need to pass
        # `self=template_method` to the renderer function. But that function is an
        # instance method, so passing `self` will override the `self` argument.
        # To get around this, we replace `self.` with something that is not an
        # argument to the renderer function.

        # Add environment (render any jinja templates).
        if template_method.env:
            d: ty.Mapping[str, str] = {
                _render_string_from_template(k, template_method):
                _render_string_from_template(v, template_method)
                for k, v in template_method.env.items()
            }
            self.env(**d)

        # Patch the `template_method.install_dependencies` instance method so it can be
        # used (ie rendered) in a template and have access to the pkg_manager requested.
        def install_patch(inner_self: _BaseInstallationTemplate,
                          pkgs: ty.List[str],
                          opts: str = None) -> str:
            return _install(pkgs=pkgs, pkg_manager=self.pkg_manager)

        # mypy complains when we try to patch a class, so we do it behind its back with
        # setattr. See https://github.com/python/mypy/issues/2427
        setattr(template_method, "install",
                types.MethodType(install_patch, template_method))

        # Set pkg_manager onto the template.
        setattr(template_method, "pkg_manager", self.pkg_manager)

        # Patch the `template_method.install_dependencies` instance method so it can be
        # used (ie rendered) in a template and have access to the pkg_manager requested.
        def install_dependencies_patch(inner_self: _BaseInstallationTemplate,
                                       opts: str = None) -> str:
            # TODO: test that template with empty dependencies (apt: []) does not render
            # any installation of dependencies.
            cmd = ""
            pkgs = inner_self.dependencies(pkg_manager=self.pkg_manager)
            if pkgs:
                cmd += _install(pkgs=pkgs,
                                pkg_manager=self.pkg_manager,
                                opts=opts)
            if self.pkg_manager == "apt":
                debs = inner_self.dependencies("debs")
                if debs:
                    cmd += "\n" + _apt_install_debs(debs)
            return cmd

        # mypy complains when we try to patch a class, so we do it behind its back with
        # setattr. See https://github.com/python/mypy/issues/2427
        setattr(
            template_method,
            "install_dependencies",
            types.MethodType(install_dependencies_patch, template_method),
        )

        # Add installation instructions (render any jinja templates).
        if template_method.instructions:
            command = _render_string_from_template(
                template_method.instructions, template_method)
            # TODO: raise exception here or skip the run instruction?
            if not command.strip():
                raise RendererError(
                    f"empty rendered instructions in {template.name}")
            self.run(command)

        return self