The code for the example is in examples/hello_world_server/
.
Building and delivering software is a complex task that can be made easier with proper encapsulation: Containers. They are able to pack all required dependencies (bitwise version controlled) so that you can release code with the confidence that everything works the same way it worked on the developers machine.
The current default for building and packing software into containers is using Dockerfiles1 offered by the Docker® software. This, however, has several structural disadvantages:
- Build Container is the Deliverable Container
- Only one-dimensional caching of build steps
- No support for slimmed containers
- Unnecessary layers
Involucro [from Latin for 'envelope'] detaches the build process from the deliverable container and re-establishes proper encapsulation in containers: One Process, One Container.
From source:
$ go get github.com/involucro/involucro/cmd/involucro
$ $GOPATH/bin/involucro --version
As binary for Linux: involucro
Or, for Windows: involucro.exe involucro32.exe
And for Mac OSX: involucro.darwin
Involucro is configured by a Lua script file. By default, it is looking for
invfile.lua
in the current directory, but this can be overridden (see below).
A configuration file contains a set of tasks identified by a unique name.
These names can be specified when invoking involucro
and are executed in the
order they are given. For example, $ involucro build package
will run the
build
and afterwards the package
task. A task can be created by invoking
inv.task('<ID>')
in the configuration file.
For easy readability, the configuration file uses a fluent syntax to build the tasks. The available methods are either modifying the next registered step, or are registering a step. This type distinction is documented below for each method.
inv.task('<ID>')
(modifier) sets the task of the next registered step
to <ID>
. It makes the methods using
, runTask
, and wrap
available.
Each task consists of a list of steps that are run in the order they are given in the file. There are different types of steps. Each step has one introductory method made available from the task, a set of modifying methods setting different properties of the step, and a final registration method that registers these settings for execution. The current status can be stored at any point into a variable and reused later. However, the steps are strictly run in the order their registration method was called.
A run step executes a Docker container. By default, the current working
directory is mounted as /source
into the container which is also configured
to be the working directory of the process running in the container. It is
mainly used to transform source code using external processes such as compilers
into a different form.
task.using('<IMAGE_ID>')
(introductory) starts off a run step by
specifying the repository name (optionally with tag) or the image ID of the
image to be run. Example: task.using('gcc:4.9')
.
runstep.withConfig(<TABLE>)
(modifying) sets the values in the Lua
table as configuration values for the Docker container. The values that can be
set here are only affecting the container itself, not how is connected with the
host. See withHostConfig for this. The options available are
Config. The keys
are interpreted case insensitive. Example: runstep.withConfig({Cmd = {"/bin/echo", "Hello, World!"}})
.
runstep.withHostConfig(<TABLE>)
(modifying) sets the values in the Lua
table as host configuration values. These values control the exact execution
semantics of the container from the hosts point of view. The available options
are documented here:
HostConfig.
Example: runStep.withConfig({links = {"redis"}})
.
NOTE: By default, involucro
binds the current directory as /source
. If the
Binds
key is set in the given table it overwrites this binding. Involucro
however interprets the given bindings and changes all relative source bindings
to absolute paths. This enables bindings such as {binds = {"./dist:/data", "/tmp:/tmp"}}
.
runstep.withExpectation(<TABLE)
(modifying) registers expectations
towards the output and exit code of the process. By default, involucro
expects the process to exit cleanly with exit code 0
. Tests of executables
however may require expecting a process to fail. This can be set with the key
code
: runstep.withExpectation({code = 1})
. Similarly, an expectation
towards the output of the process on stdout
and/or stderr
can be registered
with regular expressions conforming to Re2
syntax. Example:
runstep.withExpectation({stdout = "Hello, World!\n"})
.
runstep.run('<CMD>'...
) (registration) registers the run step. The
arguments are used as the command-line arguments of the process being run. It
directly follows Docker semantics regarding process execution. Each argument is
used as a single argument for the process. Example: runstep.run('/bin/echo', 'Hello, World!')
. Note that there is no wildcard expansion or variable
replacement if the arguments are not given to a shell, such as /bin/sh
.
Example: runstep.run('/bin/sh', '-c', 'echo *')
.
A wrap step takes the contents of a directory and creates an image layer out of
it, optionally with a parent image layer and meta data. The resulting image can
be tagged into a repository with a tag name (or latest
, if none is set).
task.wrap('<SOURCE_DIR>')
(introductory) starts off a wrap step by
specifying the directory containing the files that are to be wrapped into an
image. It is also possible to use the current directory (.
). Example:
task.wrap('dist')
.
wrapstep.at('<TARGET_DIR>')
(modifying) sets the directory in the resulting
image into which the files are copied. This can be used to put HTML files into
the location the web server in the parent image expects them to be. This
directory doesn't need to exist yet. Example: wrapstep.at('/data')
.
wrapstep.inImage('<PARENT_IMAGE>')
(modifying) causes the resulting image
to be a child of the image identified by the parameter. If this modification is
omitted the resulting image is parent-less. Example:
wrapstep.inImage('nginx')
.
wrapstep.withConfig(<TABLE>)
(modifying) sets configuration values similar
to the withConfig method of the run step above. This can be used to pre-set
an entrypoint or exposed ports. Example: wrapstep.withConfig({exposedports = {"80/tcp"}})
.
wrapstep.as('<IMAGE_NAME>')
(registration) registers the step for
execution. The image constructed by the previous modifications is built and
tagged with the given name, which may include a registry designation. Example:
wrapstep.as('app:latest')
As a convenience, it is possible to run another task as part of a task. This
emulates the conventional all
task from Makefile
s. Exceptionally, the
introductory method for this step is also the registration method.
task.runTask(<ID>
) (introductory registration) registers a step that
executes the task with the given ID as part of the steps in this task. Example:
inv.task('all').runTask('compile').runTask('package')
.
Sometimes, there should be two versions of the same image sharing the same
image ID, for example to have the latest
tag equivalent to version v2
. The
tag step helps in this case.
task.tag(<NAME>
) (introductory) starts a tagging by setting the name of
the original image. This can be anything Docker accepts, including
test/asd:v2
, but also actual image IDs. Example: task.tag('test/asd')
.
tagstep.as(<NAME>
) (registration) registers a step that tags the image
named in introductory method to the name given as parameter. Example:
tagstep.as('test/asd')
.
Control files can change their behaviour during tasks with hooks, for example
to use the results of previous tasks with io.lines
.
task.hook(<FUNCTION>
) (introductory registration) registers a step
that, when taken, runs the given function. No arguments are passed, and any
return values are ignored. Example: task.hook(function () print('in step') end)
.
Tagged images can be pushed to repositories where they can be pulled by other users. This step may involve authentication, see below for details.
**task.push(<NAME>
) (introductory registration) registers a step that, when
taken, pushes the image with the given name to a remote repository. Note that
the default Docker rules apply with regard to names: If a name starts with a
server address the image is pushed there, and if not Docker Hub is selected.
Example: task.push('image:latest')
Pushes and pulls to a remote registry or to Docker Hub may be neccessary to be
authenticated. Involucro solves this by reading a JSON file in the home
directory of the current user called .involucro
. This file (currently) only
contains information about authentication, but more uses may be introduced
later.
To configure username, password, and email place a file of the following form
into $HOME/.involucro
:
{
"auths": [
"https://USERNAME:PASSWORD@SERVER.COM/?email=EMAIL@EXAMPLE.COM"
]
}
Multiple entries are possible, but only one per server. By definition, the
address for Docker Hub is index.docker.io/v1/
, so the configuration for a
user alice with password b0b and email address alice@devs.io on Docker
Hub is:
{
"auths": [
"https://alice:b0b@index.docker.io/v1/?email=alice@devs.io"
]
}
Please keep this file hidden from any user except you as it contains the password in plaintext!
Docker® is a registered trademark of Docker, Inc.