Code taken from Hercules CI repo
- renamed to Arion - minor changes - readme WIP
This commit is contained in:
commit
dc294b034e
9 changed files with 456 additions and 0 deletions
48
README.md
Normal file
48
README.md
Normal 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
26
default.nix
Normal 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
199
src/arion
Executable 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
|
2
src/arion-image/Dockerfile
Normal file
2
src/arion-image/Dockerfile
Normal file
|
@ -0,0 +1,2 @@
|
|||
FROM scratch
|
||||
COPY passwd /etc/passwd
|
2
src/arion-image/passwd
Normal file
2
src/arion-image/passwd
Normal 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
|
49
src/nix/docker-compose-module.nix
Normal file
49
src/nix/docker-compose-module.nix
Normal 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;
|
||||
};
|
||||
};
|
||||
}
|
33
src/nix/eval-docker-compose.nix
Normal file
33
src/nix/eval-docker-compose.nix
Normal 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
|
23
src/nix/service-host-store.nix
Normal file
23
src/nix/service-host-store.nix
Normal 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
74
src/nix/service.nix
Normal 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;
|
||||
};
|
||||
}
|
Loading…
Reference in a new issue