Singularity Python Recipes

We will here discuss the Singularity Python recipe writers and parsers that will help you to convert between Singularity and Docker recipes. First, let’s define what these things are:

Now we can answer what kind of things might you want to do:

Important Singularity Python added support for parsing multistage builds for version 0.0.83 and after. By default, any base layer that isn’t named is called spython-base unless you have named it otherwise.

Command Line Client

You don’t need to interact with Python to use the converter! It’s sometimes much easier to use the command line and spit something out into the terminal, for quick visual inspection or piping into an output file. If you use the spython utility, you can see your options available:

spython recipe --help

usage: spython recipe [-h] [--entrypoint ENTRYPOINT] [--json] [--force]
                      [--parser {auto,docker,singularity}]
                      [--writer {auto,docker,singularity}]
                      [files [files ...]]

positional arguments:
  files                 the recipe input file and [optional] output file

optional arguments:
  -h, --help            show this help message and exit
  --entrypoint ENTRYPOINT
                        define custom entry point and prevent discovery
  --json                dump the (base) recipe content as json to the terminal
  --force               if the output file exists, overwrite.
  --parser {auto,docker,singularity}
                        Is the input a Dockerfile or Singularity recipe?
  --writer {auto,docker,singularity}
                        Should we write to Dockerfile or Singularity recipe?

Auto Detection

The most basic usage is auto generation - meaning you provide a Dockerfile or Singularity recipe, and we automatically detect it and convert to the other type. Until we add additional writers and/or parsers, this is reasonable to do:

$ spython recipe Dockerfile
Bootstrap: docker
From: python:3.5.1
...

Instead of printing to the screen. we can provide a filename to write to file:

$ spython recipe Dockerfile Singularity.snowflake

The same auto-detection can be done for converting a Dockerfile to Singularity

$ spython recipe Singularity
$ spython recipe Singularity Dockerfile

And don’t forget you can interact with Docker images natively with Singularity!

$ singularity pull docker://ubuntu:latest

Customize Writers and Parsers

If you want to specify the writer or parser to use, this can be done with the --writer and --parser argument, respectively. The following would convert a Dockerfile into a version of itself:

$ spython recipe --writer docker Dockerfile

or if our file is named something non-traditional, we would need to specify the parser too:

$ spython recipe --parser singularity container.def

Custom Entrypoint

Another customization to a recipe can be modifying the entrypoint on the fly.

$ spython recipe --entrypoint /bin/sh Dockerfile
...
%runscript
exec /bin/sh "$@"

Debug Generation

Finally, you can ask for help and print with more verbosity! Just ask for --debug

$ spython --debug recipe Dockerfile
DEBUG Logging level DEBUG
DEBUG Singularity Python Version: 0.0.63
DEBUG [in]  FROM python:3.5.1
DEBUG FROM python:3.5.1
DEBUG [in]  ENV PYTHONUNBUFFERED 1
DEBUG [in]  RUN apt-get update && apt-get install -y \
DEBUG [in]      pkg-config \
DEBUG [in]      cmake \
DEBUG [in]      openssl \
DEBUG [in]      wget \
DEBUG [in]      git \
DEBUG [in]      vim
...

or less, ask for --quiet

$ spython --quiet recipe Dockerfile

Python API

Recipes

If you want to create a generic recipe (without association with a container technology) you can do that.

from spython.main.parse.recipe import Recipe
recipe = Recipe()

By default, the recipe starts empty.

recipe.json()
{}

Generally, you can inspect the attributes to see what can be added! Here are some examples:

recipe.cmd = ['echo', 'hello']
recipe.entrypoint = '/bin/bash'
recipe.comments = ['This recipe is great', 'Yes it is!']
recipe.environ = ['PANCAKES=WITHSYRUP']
recipe.files = [['one', 'two']]
recipe.test = ['true']
recipe.install = ['apt-get update']
recipe.labels = ['Maintainer vanessasaur']
recipe.ports = ['3031']
recipe.volumes = ['/data']
recipe.workdir = '/code'
recipe.fromHeader =  'ubuntu:18:04'

And then verify they are added:

recipe.json()
{'cmd': ['echo', 'hello'],
 'comments': ['This recipe is great', 'Yes it is!'],
 'entrypoint': '/bin/bash',
 'environ': ['PANCAKES=WITHSYRUP'],
 'files': [['one', 'two']],
 'install': ['apt-get update'],
 'labels': ['Maintainer vanessasaur'],
 'ports': ['3031'],
 'test': ['true'],
 'volumes': ['/data'],
 'workdir': '/code'}

And then you can use a writer to print a custom recipe type to file. Note that the writer is intended for multistage builds, meaning that the recipe you provide it should be a lookup with sections. For example:

from spython.main.parse.writers import DockerWriter
writer = DockerWriter({"baselayer": recipe})

FROM ubuntu:18:04 AS baselayer
ADD one two
LABEL Maintainer vanessasaur
ENV PANCAKES=WITHSYRUP
RUN apt-get update
EXPOSE 3031
WORKDIR /code
CMD ['echo', 'hello']
ENTRYPOINT /bin/bash
HEALTHCHECK true

Parsers

Your first interaction will be with a parser, all of which are defined at spython.main.parse.parsers. If you know the parser you want directly, you can import it:

from spython.main.parse.parsers import DockerParser

or you can use a helper function to get it:

from spython.main.parse.parsers import get_parser
DockerParser = get_parser('docker')
# spython.main.parse.parsers.docker.DockerParser

then give it a Dockerfile to munch on.

parser=DockerParser('Dockerfile')

By default, it will parse the Dockerfile (or other container recipe) into a lookup of Recipe class, each of which is a layer / stage for the build.

> parser.recipe
{'builder': [spython-recipe][source:/home/vanessa/Desktop/Code/singularity-cli/Dockerfile],
 'runner': [spython-recipe][source:/home/vanessa/Desktop/Code/singularity-cli/Dockerfile]}

In the above, we see that the Dockerfile has two staged, the first named builder and the second named runner. You can inspect each of these recipes by indexing into the dictionary. E.g., here is how to look at the .json output as we did previously:

parser.recipe['runner'].json()
Out[6]:
{'fromHeader': 'ubuntu:20.04 ',
 'layer_files': {'builder': [['/build_thirdparty/usr/', '/usr/'],
   ['/build${PROJ_INSTALL_PREFIX}/share/proj/',
    '${PROJ_INSTALL_PREFIX}/share/proj/'],
   ['/build${PROJ_INSTALL_PREFIX}/include/',
    '${PROJ_INSTALL_PREFIX}/include/'],
   ['/build${PROJ_INSTALL_PREFIX}/bin/', '${PROJ_INSTALL_PREFIX}/bin/'],
   ['/build${PROJ_INSTALL_PREFIX}/lib/', '${PROJ_INSTALL_PREFIX}/lib/'],
   ['/build/usr/share/gdal/', '/usr/share/gdal/'],
   ['/build/usr/include/', '/usr/include/'],
   ['/build_gdal_python/usr/', '/usr/'],
   ['/build_gdal_version_changing/usr/', '/usr/']]},
 'install': ['\n',
  'date',
  '\n',
  '# PROJ dependencies\n',
  'apt-get update; \\',
  'DEBIAN_FRONTEND=noninteractive apt-get install -y \\',
   ...
  '\n',
  'ldconfig']}

Notice in the above that we have a section called layer_files that a writer knows how to parse into a %files section from the previous layer. All of these fields are attributes of the recipe, so you could change or otherwise interact with them. For example, here we are adding an entrypoint.

parser.recipe['runner'].entrypoint = '/bin/sh'

or if you don’t want to, you can skip automatic parsing. Here we inspect a single, empty recipe layer:

parser = DockerParser('Dockerfile', load=False) parser.recipe
{'spython-base': [spython-recipe][source:/home/vanessa/Desktop/Code/singularity-cli/Dockerfile]}

parser.recipe['spython-base'].json()
{}

WHen you are ready to parse it (to show the layers we saw previously)

parser.parse()

The same is available for Singularity recipes:

SingularityParser = get_parser("Singularity")
parser = SingularityParser("Singularity")
parser.recipe['spython-base'].json()
Out[21]:
{'cmd': 'exec /opt/conda/bin/spython "$@"',
 'fromHeader': 'continuumio/miniconda3',
 'install': ['apt-get update && apt-get install -y git',
  '# Dependencies',
  'cd /opt',
  'git clone https://www.github.com/singularityhub/singularity-cli',
  'cd singularity-cli',
  '/opt/conda/bin/pip install setuptools',
  '/opt/conda/bin/python setup.py install'],
 'labels': ['maintainer vsochat@stanford.edu']}

Writers

Once you have loaded a recipe and possibly made changes, what comes next? You would want to write it to a possibly different recipe file. For example, let’s read in some Dockerfile, and then hand off the recipe to a SingularityWriter. The same functions are available to get a writer, or you can import directly.

from spython.main.parse.writers import get_writer
from spython.main.parse.parsers import get_parser

DockerParser = get_parser('docker')
SingularityWriter = get_writer('singularity')
# from spython.main.parse.writers import SingularityWriter

First, again parse the Dockerfile:

parser = DockerParser('Dockerfile')

And then give the recipe object at parser.recipe to the writer!

writer = SingularityWriter(parser.recipe)

How do you generate the new recipe? You can do:

writer.convert()

To better print it to the screen, you can use print:

print(writer.convert())
Bootstrap: docker
From: python:3.5.1
%files
requirements.txt /tmp/requirements.txt
...

%environment
export PYTHONUNBUFFERED=1
%runscript
cd /code
exec /bin/bash/bin/bash /code/run_uwsgi.sh "$@"
%startscript
cd /code
exec /bin/bash/bin/bash /code/run_uwsgi.sh "$@"

Or return to a string, and save to file as you normally would.

result = writer.convert()

The same works for a DockerWriter.

SingularityParser = get_parser('singularity')
DockerWriter = get_writer('docker')
parser = SingularityParser('Singularity')
writer = DockerWriter(parser.recipe)
print(writer.convert())
FROM continuumio/miniconda3 AS spython-base
LABEL maintainer vsochat@stanford.edu
RUN apt-get update && apt-get install -y git
RUN cd /opt
RUN git clone https://www.github.com/singularityhub/singularity-cli
RUN cd singularity-cli
RUN /opt/conda/bin/pip install setuptools
RUN /opt/conda/bin/python setup.py install
CMD exec /opt/conda/bin/spython "$@"