Code taken from Hercules CI repo

- renamed to Arion
 - minor changes
 - readme WIP
This commit is contained in:
Robert Hensing 2018-12-17 19:08:38 +01:00
commit dc294b034e
9 changed files with 456 additions and 0 deletions

48
README.md Normal file
View file

@ -0,0 +1,48 @@
# Run docker-compose without images with Nix
*Wait, what?*
With Arion you can fire up containers without creating images for each
service. Instead, it uses a mostly empty image, sort of like a base
image, and makes the host's Nix store available in the container,
allowing the container to run programs without having to re-package
them into a docker image.
Arion is configured using Nix with modules, like those in
NixOS. Similar to `docker-compose` it can therefore combine
configuration from multiple files. For managing the network and
containers it delegates to the `docker-compose` command.
# Project Status
This project was born out of a process supervision need for local
development environments while working
on [Hercules CI](https://www.hercules-ci.com). (It was also born out
of ancient Greek deities disguised as horses. More on that later.)
We don't use it to deploy to 'real' environments and we have no plans
to do so in the future.
If you do want to use Arion for 'real' environments, you'll probably
want to either build images or manage garbage collection roots if you
control the deployment host. Either of these has yet to be
implemented.
# How do I use Arion?
The command line inherits most `docker-compose` commands. These
examples assume you're in this repo's `examples/` directory.
TODO examples
# Why "Arion"?
Arion comes from Greek mythology. Poseidon, the god of ~Docker~ the
seas had his eye on Demeter. Demeter tried to trick him by disguising
as a horse, but Poseidon saw through the deception and they had Arion.
So Arion is a super fast divine horse; the result of some weird
mixing. Also it talks.
(And we feel morally obliged to name our stuff after Greek mythology)

26
default.nix Normal file
View file

@ -0,0 +1,26 @@
{ pkgs ? import <nixpkgs> {} }:
let
inherit (pkgs) lib stdenv;
arion = stdenv.mkDerivation {
name = "arion";
src = ./src;
unpackPhase = "";
buildPhase = "";
installPhase = ''
mkdir -p $out/bin $out/share/arion
cp -a nix $out/share/arion/
cp -a arion-image $out/share/arion/
substitute arion $out/bin/arion \
--subst-var-by path ${lib.makeBinPath [pkgs.jq pkgs.coreutils]} \
--subst-var-by nix_dir $out/share/arion/nix \
;
chmod a+x $out/bin/arion
'';
};
in
{
inherit arion;
}

199
src/arion Executable file
View file

@ -0,0 +1,199 @@
#!/usr/bin/env bash
set -euo pipefail
export PATH="$PATH:@path@"
nix_dir="@nix_dir@"
docker_compose_args=()
files=()
command="docker-compose"
pkgs_argument="./arion-pkgs.nix"
debug() {
# echo "$@"
:
}
while test $# != 0; do
case "$1" in
-f|--file)
shift
files+=("$1")
;;
-f*)
files+=("${1/#-f/}")
;;
--file=*)
files+=("${1/#--file=}")
;;
--pkgs)
shift
pkgs_argument="$1"
;;
-h|--help|help)
command="help"
shift
break
;;
cat)
command="$1"
shift
break
;;
repl)
command="$1"
shift
break
;;
docker-compose)
command="docker-compose"
shift
break
;;
*)
break
;;
esac
shift
done
while test $# != 0; do
docker_compose_args+=("$1")
shift
done
case "$command" in
help)
cat <<EOF
Arion wraps your system's docker-compose, providing a NixOps-like
experience for simple container deployments.
Usage:
arion up|logs|... - execute docker-compose commands
arion cat - display raw docker-compose.yaml
arion config - validate and display the config file
arion repl - explore the config interactively
arion help
arion docker-compose help
arion docker-compose help up|logs|...
EOF
exit 0
;;
*)
;;
esac
if [[ ${#files[@]} == 0 ]]; then
files=("./arion-compose.nix")
fi
debug docker_compose_args: "${docker_compose_args[@]}"
debug files: "${files[@]}"
docker_compose_yaml=.tmp-nix-docker-compose-$$-$RANDOM.yaml
cleanup() {
rm -f $docker_compose_yaml
}
trap cleanup EXIT
modules="["
for file in "${files[@]}"; do
case "$file" in
/*)
modules="$modules (/. + $(printf '"%q"' $file))"
;;
*)
modules="$modules (./. + $(printf '"/%q"' $file))"
;;
esac
done
modules="$modules ]"
debug modules: $modules
old_IFS="$IFS"
IFS=""
args=(
)
IFS="$old_IFS"
for arg in "${args[@]}"; do
echo arg: $arg
done
do_build() {
echo 1>&2 "Building configuration..."
nix-build \
"$nix_dir/eval-docker-compose.nix" \
--out-link $docker_compose_yaml \
--argstr uid "$UID" \
--arg modules "$modules" \
--arg pkgs "$pkgs_argument" \
--show-trace \
--attr 'config.build.dockerComposeYaml' \
>/dev/null ;
}
do_repl() {
# nix repl doesn't autocall its <FILES> arguments
# so we improvise. We need a file in this directory
# to make sure that all paths are as expected :(
trap do_repl_cleanup EXIT;
REPL_TMP=.tmp-repl-$$-$RANDOM
cat <<EOF
Launch a repl for you, using a temporary file: $REPL_TMP.
This loads the configuration from the modules
$files
To get started:
To see deployment-wide configuration
type config. and hit TAB
To see the services
type config.docker-compose.services TAB or ENTER
To bring the top-level Nixpkgs attributes into scope
type :a (config._module.args.pkgs) // { inherit config; }
EOF
cat >"$REPL_TMP" <<EOF
import $nix_dir/eval-docker-compose.nix {
uid = "$UID";
modules = $modules;
pkgs = $pkgs_argument;
}
EOF
nix repl \
"$REPL_TMP" \
;
}
do_repl_cleanup() {
rm -f $REPL_TMP
}
case "$command" in
cat)
do_build
cat $docker_compose_yaml | jq .
;;
repl)
do_repl
;;
docker-compose)
if [[ ${#docker_compose_args[@]} != 0
&& ${docker_compose_args[0]} != "help"
&& ${docker_compose_args[0]} != "version"
]]; then
do_build
fi
docker-compose -f $docker_compose_yaml "${docker_compose_args[@]}"
;;
esac

View file

@ -0,0 +1,2 @@
FROM scratch
COPY passwd /etc/passwd

2
src/arion-image/passwd Normal file
View file

@ -0,0 +1,2 @@
root:x:0:0:System administrator:/root:/bin/sh
nobody:x:65534:65534:Unprivileged account (don't use!):/var/empty:/run/current-system/sw/bin/nologin

View file

@ -0,0 +1,49 @@
{ pkgs, uid, lib, config, ... }:
let
evalService = name: modules:
let
composite = lib.evalModules {
check = true;
modules = builtinModules ++ modules;
};
builtinModules = [
argsModule
./service.nix
./service-host-store.nix
];
argsModule = {
_file = ./docker-compose-module.nix;
key = ./docker-compose-module.nix;
config._module.args.pkgs = lib.mkForce pkgs;
config._module.args.uid = uid;
};
in
composite.config.build.service;
in
{
options = {
build.dockerComposeYaml = lib.mkOption {
type = lib.types.package;
};
docker-compose.raw = lib.mkOption {
type = lib.types.attrs;
};
docker-compose.services = lib.mkOption {
default = {};
type = with lib.types; attrsOf (coercedTo unspecified (a: [a]) (listOf unspecified));
};
};
config = {
build.dockerComposeYaml = pkgs.writeText "docker-compose.yaml" (builtins.toJSON (config.docker-compose.raw));
docker-compose.raw = {
version = "3";
services = lib.mapAttrs evalService config.docker-compose.services;
};
};
}

View file

@ -0,0 +1,33 @@
{ modules ? [], uid ? 0, pkgs }:
let _pkgs = pkgs;
in
let
pkgs = if builtins.typeOf _pkgs == "path"
then import _pkgs
else if builtins.typeOf _pkgs == "set"
then _pkgs
else builtins.abort "The pkgs argument must be an attribute set or a path to an attribute set.";
inherit (pkgs) lib;
composite = lib.evalModules {
check = true;
modules = builtinModules ++ modules;
};
builtinModules = [
argsModule
./docker-compose-module.nix
];
argsModule = {
_file = ./eval-docker-compose.nix;
key = ./eval-docker-compose.nix;
config._module.args.pkgs = lib.mkIf (pkgs != null) (lib.mkForce pkgs);
config._module.args.uid = uid;
};
in
# Typically you need composite.config.build.dockerComposeYaml
composite

View file

@ -0,0 +1,23 @@
# Bind-mounts the host store
{ lib, config, ... }:
let
inherit (lib) mkOption types mkIf;
in
{
options = {
service.useHostStore = mkOption {
type = types.bool;
default = false;
description = "Bind mounts the host store if enabled, avoiding copying.";
};
};
config = mkIf config.service.useHostStore {
service.image = "arion-base";
service.build.context = "${../arion-image}";
service.volumes = [
"/nix/store:/nix/store"
"/bin/sh:/bin/sh"
"/usr/bin/env:/usr/bin/env"
];
};
}

74
src/nix/service.nix Normal file
View file

@ -0,0 +1,74 @@
{ pkgs, lib, config, ... }:
let
inherit (lib) mkOption types;
inherit (types) listOf nullOr attrsOf string either;
in
{
options = {
service.volumes = mkOption {
type = listOf types.unspecified;
default = [];
};
service.build.context = mkOption {
type = nullOr string;
default = null;
};
service.environment = mkOption {
type = attrsOf string;
default = {};
};
service.image = mkOption {
type = string;
};
service.command = mkOption {
type = nullOr types.unspecified;
default = null;
};
service.depends_on = mkOption {
type = listOf string;
default = [];
};
service.restart = mkOption {
type = nullOr string;
default = null;
};
service.ports = mkOption {
type = listOf types.unspecified;
default = [];
description = ''
Expose ports on host. "host:container" or structured.
See https://docs.docker.com/compose/compose-file/#ports
'';
};
service.expose = mkOption {
type = listOf string;
default = [];
};
build.service = mkOption {
type = attrsOf types.unspecified;
};
};
config.build.service = {
inherit (config.service)
volumes
environment
image
;
} // lib.optionalAttrs (config.service.build.context != null) {
inherit (config.service) build;
} // lib.optionalAttrs (config.service.command != null) {
inherit (config.service) command;
} // lib.optionalAttrs (config.service.depends_on != []) {
inherit (config.service) depends_on;
} // lib.optionalAttrs (config.service.restart != null) {
inherit (config.service) restart;
} // lib.optionalAttrs (config.service.ports != []) {
inherit (config.service) ports;
} // lib.optionalAttrs (config.service.expose != []) {
inherit (config.service) expose;
};
}