#!/bin/bash

##
## TODO:
## - subordinate container should really be able to modify base image of their master
##   - this could be done through docker-update
## - I'm not happy with the current build using 'build/' directory, this should be
##   changed to:
##   - always have a base image (specified in metadata), and always have hooks/install
##     executed and merge in image (like docker-build-charm).
##   - container base image is ALWAYS the image of the master container... this brings
##     questions about a double way to express inheritage (through relations as it is
##     implemented now, or through this base-image ?)
## - the name of the scripts for relation (aka relation_name-relation-joined) is bad as
##   reading the name in a hooks/ dir, there are no way to know if we are the target or
##   the base of the relation.
##   - we could leverage a 'relations/' dir on the root of the charm, with both:
##     'relations/provide/relation_name' and 'relations/receive/relation_name'
##   - a very bad point with the actual naming is that we can't have a providing AND
##     receiving a relation with same name.
## - The cache system should keep md5 of docker-compose and other things between runs
## - The cache system should use underlying function that have only arguments inputs.
##   This will allow to cache completely without issues function in time.
## - would probably need instrospection in charm custom action to know if these need
##   init or relations to be set up.
## - Be clear about when the SERVICE name is used and the CHARM name is used.
##   - in case of service contained in another container
##   - in normal case
##   - in docker-compose, can't use charm name: if we want 2 instances of the same charm
##     we are stuck. What will be unique is the name of the service.
## - some relations are configured in compose.yml but should not trigger the loading
##   of necessary component (for instance, apache --> log-rotate), if log-rotate is
##   not there, this link should considered optional.
## - Could probably allow an unexistent charm to be populated with only "docker-image:"
##   of the same name. Although this should trigger a visible warning.


#:-
[ -e /etc/shlib ] && . /etc/shlib || {
          echo "Unsatisfied dependency. Please install 'kal-shlib-core'."
          exit 1
      }
#:-

include common
include pretty
include parse
include charm
include array
include cla

depends shyaml docker

exname="compose"
version=0.1
usage="$exname [COMPOSE_OPTS] [ACTION [ACTION_OPTS]]"
help="\

$WHITE$exname$NORMAL jobs is to run various shell scripts to build
a running orchestrated and configured docker containers. These shell
scripts will have the opportunity to build a 'docker-compose.yml'.

Once init script and relations scripts are executed, $WHITE$exname$NORMAL
delegate the launching to ${WHITE}docker-compose${NORMAL} by providing it
the final 'docker-compose.yml'.

$WHITE$exname$NORMAL also leverage charms to offer some additional custom
actions per charm, which are simply other scripts that can be
run without launching ${WHITE}docker-compose${NORMAL}.

In compose message, color coding is enforced as such:
 - ${DARKCYAN}action$NORMAL,
 - ${DARKBLUE}relation$NORMAL,
 - ${DARKPINK}charm${NORMAL},
 - ${DARKYELLOW}service${NORMAL},
 - ${WHITE}option-name${NORMAL}/${WHITE}command-name${NORMAL}/${WHITE}Section-Title${NORMAL}

$WHITE$exname$NORMAL reads '/etc/compose.conf' for global variables, and
'/etc/compose.local.conf' for local host adjustements.

"


## XXXvlab: this doesn't seem to work when 'compose' is called in
## a hook of a charm.
#[[ "${BASH_SOURCE[0]}" == "" ]] && SOURCED=true
$(return >/dev/null 2>&1) && SOURCED=true


if [ "$UID" == 0 ]; then
    CACHEDIR=${CACHEDIR:-/var/cache/compose}
    VARDIR=${VARDIR:-/var/lib/compose}
else
    [ "$XDG_CONFIG_HOME" ] && CACHEDIR=${CACHEDIR:-$XDG_CONFIG_HOME/compose}
    [ "$XDG_DATA_HOME" ] && VARDIR=${VARDIR:-$XDG_DATA_HOME/compose}
    CACHEDIR=${CACHEDIR:-$HOME/.cache/compose}
    VARDIR=${VARDIR:-$HOME/.local/share/compose}
fi
export VARDIR CACHEDIR


md5_compat() { md5sum | cut -c -32; }
quick_cat_file() { quick_cat_stdin < "$1"; }
quick_cat_stdin() { local IFS=''; while read -r line; do echo "$line"; done ; }
export -f quick_cat_file quick_cat_stdin md5_compat


clean_cache() {
    local i=0
    for f in $(ls -t "$CACHEDIR/"*.cache.* 2>/dev/null | tail -n +500); do
        ((i++))
        rm -f "$f"
    done
    if (( i > 0 )); then
        debug "${WHITE}Cleaned cache:${NORMAL} Removed $((i)) elements (current cache size is $(du -sh "$CACHEDIR" | cut -f 1))"
    fi
}


usage="$exname SERVICE"'

Deploy and manage a swarm of containers to provide services based on
a ``compose.yml`` definition and charms from a ``charm-store``.
'

export DEFAULT_COMPOSE_FILE

##
## Merge YAML files
##

export _merge_yaml_common_code="

import sys
import yaml

try:
    from yaml import CSafeLoader as SafeLoader, CSafeDumper as SafeDumper
except ImportError:  ## pragma: no cover
    sys.stderr.write('YAML code in pure python\n')
    exit(1)
    from yaml import SafeLoader, SafeDumper

class MySafeLoader(SafeLoader): pass
class MySafeDumper(SafeDumper): pass


try:
    # included in standard lib from Python 2.7
    from collections import OrderedDict
except ImportError:
    # try importing the backported drop-in replacement
    # it's available on PyPI
    from ordereddict import OrderedDict


## Ensure that there are no collision with legacy OrderedDict
## that could be used for omap for instance.
class MyOrderedDict(OrderedDict):
    pass

MySafeDumper.add_representer(
    MyOrderedDict,
    lambda cls, data: cls.represent_dict(data.items()))


def construct_omap(cls, node):
    cls.flatten_mapping(node)
    return MyOrderedDict(cls.construct_pairs(node))


MySafeLoader.add_constructor(
    yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
    construct_omap)


##
## Support local and global objects
##

class EncapsulatedNode(object): pass


def mk_encapsulated_node(s, node):

    method = 'construct_%s' % (node.id, )
    data = getattr(s, method)(node)

    class _E(data.__class__, EncapsulatedNode):
        pass

    _E.__name__ = str(node.tag)
    _E._node = node
    return _E(data)


def represent_encapsulated_node(s, o):
    value = s.represent_data(o.__class__.__bases__[0](o))
    value.tag = o.__class__.__name__
    return value


MySafeDumper.add_multi_representer(EncapsulatedNode,
                                       represent_encapsulated_node)
MySafeLoader.add_constructor(None, mk_encapsulated_node)


def fc(filename):
    with open(filename) as f:
        return f.read()

def merge(*args):
    # sys.stderr.write('%r\n' % (args, ))
    args = [arg for arg in args if arg is not None]
    if len(args) == 0:
        return None
    if len(args) == 1:
        return args[0]
    if all(isinstance(arg, (int, basestring, bool)) for arg in args):
        return args[-1]
    elif all(isinstance(arg, list) for arg in args):
        res = []
        for arg in args:
            for elt in arg:
                if elt in res:
                    res.remove(elt)
                res.append(elt)
        return res
    elif all(isinstance(arg, dict) for arg in args):
        keys = set()
        for arg in args:
            keys |= set(arg.keys())
        dct = {}
        for key in keys:
            sub_args = []
            for arg in args:
                if key in arg:
                    sub_args.append(arg)
            try:
                dct[key] = merge(*(a[key] for a in sub_args))
            except NotImplementedError as e:
                raise NotImplementedError(
                    e.args[0],
                    '%s.%s' % (key, e.args[1]) if e.args[1] else key,
                    e.args[2])
            if dct[key] is None:
                del dct[key]
        return dct
    else:
        raise NotImplementedError(
            'Unsupported types: %s'
            % (', '.join(list(set(arg.__class__.__name__ for arg in args)))), '', args)
    return None

def merge_cli(*args):
    try:
        c = merge(*args)
    except NotImplementedError as e:
        sys.stderr.write('Merging Failed: %s.\n%s\n'
            '  Values are:\n    %s\n'
            % (e.args[0],
               '  Conflicting key is %r.' % e.args[1] if e.args[1] else
                   '  Conflict at base of structure.',
               '\\n    '.join('v%d: %r' % (i, a)
                         for i, a in enumerate(e.args[2]))))
        exit(1)
    if c is not None:
        print '%s' % yaml.dump(c, default_flow_style=False, Dumper=MySafeDumper)

"


merge_yaml() {

    if ! [ -r "$state_tmpdir/merge_yaml.py" ]; then
        cat <<EOF > "$state_tmpdir/merge_yaml.py"

$_merge_yaml_common_code

merge_cli(*(yaml.load(fc(f), Loader=MySafeLoader) for f in sys.argv[1:]))
EOF
    fi

    python "$state_tmpdir/merge_yaml.py" "$@"
}
export -f merge_yaml


merge_yaml_str() {
    local entries="$@"

    if ! [ -r "$state_tmpdir/merge_yaml_str.py" ]; then
        cat <<EOF > "$state_tmpdir/merge_yaml_str.py" || return 1

$_merge_yaml_common_code

merge_cli(*(yaml.load(f, Loader=MySafeLoader) for f in sys.argv[1:]))
EOF
    fi

    if ! python "$state_tmpdir/merge_yaml_str.py" "$@"; then
        err "Failed to merge yaml strings:"
        local s
        for s in "$@"; do
            printf " - \n"
            printf "%s\n" "$s" | prefix "  ${GRAY}|$NORMAL "
        done >&2
        return 1
    fi
}
export -f merge_yaml_str


yaml_key_val_str() {
    local entries="$@"

    if ! [ -r "$state_tmpdir/yaml_key_val_str.py" ]; then
        cat <<EOF > "$state_tmpdir/yaml_key_val_str.py"

$_merge_yaml_common_code

print '%s' % yaml.dump(
  {
    yaml.load(sys.argv[1], Loader=MySafeLoader):
    yaml.load(sys.argv[2], Loader=MySafeLoader)
  },
  default_flow_style=False,
  Dumper=MySafeDumper,
  )

EOF
    fi

    python "$state_tmpdir/yaml_key_val_str.py" "$@"
}
export -f yaml_key_val_str


##
## Docker
##

docker_has_image() {
    local image="$1"
    images=$(docker images -q "$image" 2>/dev/null) || {
        err "docker images call has failed unexpectedly."
        return 1
    }
    [ "$images" ]
}
export -f docker_has_image


docker_image_id() {
    local image="$1" image_id cache_file="$state_dir/$FUNCNAME.cache.$(echo "$*" | md5_compat)"
    if [ -e "$cache_file" ]; then
        # debug "$FUNCNAME: cache hit ($*)"
        quick_cat_stdin < "$cache_file"
        return 0
    fi
    image_id=$(docker inspect "$image" --format='{{.Id}}') || return 1
    echo "$image_id" | tee "$cache_file"
}
export -f docker_image_id


cached_cmd_on_image() {
    local image="$1" cache_file
    image_id=$(docker_image_id "$image") || return 1
    cache_file="$CACHEDIR/$FUNCNAME.cache.$(echo "$*" | md5_compat)"
    if [ -e "$cache_file" ]; then
        # debug "$FUNCNAME: cache hit ($*)"
        quick_cat_stdin < "$cache_file"
        return 0
    fi
    shift
    out=$(docker run -i --rm --entrypoint /bin/sh "$image_id" -c "$*") || return 1
    echo "$out" | tee "$cache_file"
}
export -f cached_cmd_on_image


cmd_on_base_image() {
    local service="$1" base_image
    shift
    base_image=$(service_base_docker_image "$service") || return 1
    docker run -i --rm --entrypoint /bin/bash "$base_image" -c "$*"
}
export -f cmd_on_base_image


cached_cmd_on_base_image() {
    local service="$1" base_image cache_file="$state_tmpdir/$FUNCNAME.cache.$(printf "%s\0" "$@" | md5_compat)"
    shift
    if [ -e "$cache_file" ]; then
        # debug "$FUNCNAME: cache hit ($*)"
        quick_cat_stdin < "$cache_file"
        return 0
    fi
    base_image=$(service_base_docker_image "$service") || return 1
    if ! docker_has_image "$base_image"; then
        docker pull "$base_image"
    fi
    result=$(cached_cmd_on_image "$base_image" "$@") || return 1
    echo "$result" | tee "$cache_file"
}
export -f cached_cmd_on_base_image


image_exposed_ports_0() {
    local image="$1"
    docker inspect --format='{{range $p, $conf := .Config.ExposedPorts}}{{$p}}{{"\x00"}}{{end}}' "$image"
}
export -f image_exposed_ports_0

##
## Generic
##


fn.exists() {
    declare -F "$1" >/dev/null
}


str_pattern_matches() {
    local str="$1"
    shift
    for pattern in "$@"; do
        eval "[[ \"$str\" == $pattern ]]" && return 0
    done
    return 1
}


str_matches() {
    local str="$1"
    shift
    for pattern in "$@"; do
        [[ "$str" == "$pattern" ]] && return 0
    done
    return 1
}

gen_password() {
    local l=( {a..z} {A..Z} {0..9} ) nl="${#l[@]}" size=${1:-16}
    while ((size--)); do
        echo -n "${l[$((RANDOM * nl / 32768))]}"
    done
    echo
}
export -f gen_password


file_put() {
    local TARGET="$1"
    mkdir -p "$(dirname "$TARGET")" &&
    cat - > "$TARGET"
}
export -f file_put


file_put_0() {
    local TARGET="$1"
    mkdir -p "$(dirname "$TARGET")" &&
    cat > "$TARGET"
}
export -f file_put_0


fetch_file() {
    local src="$1"

    case "$src" in
        *"://"*)
            err "Unsupported target scheme."
            return 1
            ;;
        *)
            ## Try direct
            if ! [ -r "$src" ]; then
                err "File '$src' not found/readable."
                return 1
            fi
            cat "$src" || return 1
            ;;
    esac
}
export -f fetch_file


## receives stdin content to decompress on stdout
## stdout content should be tar format.
uncompress_file() {
    local filename="$1"

    ## Warning, the content of the file is already as stdin, the filename
    ## is there to hint for correct decompression.
    case "$filename" in
        *".gz")
            gunzip
            ;;
        *".bz2")
            bunzip2
            ;;
        *)
            cat
            ;;
    esac

}
export -f uncompress_file


get_file() {
    local src="$1"

    fetch_file "$src" | uncompress_file "$src"
}
export -f get_file


##
## Common database lib
##

_clean_docker() {
    local _DB_NAME="$1" container_id="$2"
    (
        set +e
        debug "Removing container $_DB_NAME"
        docker stop "$container_id"
        docker rm "$_DB_NAME"
        docker network rm "${_DB_NAME}"
        rm -vf "$state_tmpdir/${_DB_NAME}.state"
    )
}
export -f _clean_docker


get_service_base_image_dir_uid_gid() {
    local service="$1" dir="$2" uid_gid
    uid_gid=$(cached_cmd_on_base_image "$service" "stat -c '%u %g' '$dir'") || {
        debug "Failed to query '$dir' uid in ${DARKYELLOW}$service${NORMAL} base image."
        return 1
    }
    info "uid and gid from ${DARKYELLOW}$service${NORMAL}:$dir is '$uid_gid'"
    echo "$uid_gid"
}
export -f get_service_base_image_dir_uid_gid


get_service_type() {
    local service="$1" cache_file="$state_tmpdir/$FUNCNAME.cache.$1"
    if [ -z "$service" ]; then
        print_syntax_error "$FUNCNAME: Please specify a service as first argument."
        return 1
    fi

    if [ -e "$cache_file" ]; then
        # debug "$FUNCNAME: cache hit ($*)"
        cat "$cache_file"
        return 0
    fi

    charm=$(get_service_charm "$service") || return 1
    metadata=$(charm.metadata "$charm") || return 1
    printf "%s" "$metadata" | shyaml get-value type service 2>/dev/null |
        tee "$cache_file"
}


are_files_locked_in_dir() {
    local dir="$1" device hdev ldev
    device=$(stat -c %d "$dir") || {
        err "Can't stat %d."
        return 1
    }
    device=$(printf "%04x" $device)
    hdev=${device:0:2}
    ldev=${device:2:2}
    inodes=$(find "$dir" -printf ':%i:\n')

    found=
    while read -r inode; do
        debug "try inode:$inode"
        if [[ "$inodes" == *":$inode:"* ]]; then
            found=1
            break
        fi
    done < <(cat /proc/locks | grep " $hdev:$ldev:" | sed -r "s/^.*$hdev:$ldev:([0-9]+).*$/\1/g")

    [ "$found" ]
}
export -f are_files_locked_in_dir


export _PID="$$"
ensure_db_docker_running () {
    local _STATE_FILE

    _DB_NAME="db_${DB_NAME}_${_PID}"
    _STATE_FILE="$state_tmpdir/${_DB_NAME}.state"
    if [ -e "$_STATE_FILE" ]; then
        IFS=: read DOCKER_NETWORK DOCKER_IP <<<"$(cat "$_STATE_FILE")"

        debug "Re-using previous docker/connection '$DOCKER_IP'."
        _set_db_params "$DOCKER_IP" "$DOCKER_NETWORK"

        return 0
    fi

    if [ -e "$state_tmpdir/${_DB_NAME}.working" ]; then
        ## avoid recursive calls.
        if [ -z "$DOCKER_IP" ]; then
            err "Currently figuring up DOCKER_IP, please set it yourself before this call if needed."
            return 1
        else
            debug "ignoring recursive call of 'ensure_db_docker_running'."
        fi
        return 0
    fi

    touch "$state_tmpdir/${_DB_NAME}.working"

    docker rm "$_DB_NAME" 2>/dev/null || true

    host_db_working_dir="$DATASTORE/${SERVICE_NAME}$DB_DATADIR"

    if is_db_locked; then
        info "Some process is using '$host_db_working_dir'. Trying to find a docker that would do this..."
        found=
        for docker_id in $(docker ps -q); do
            has_volume_mounted=$(
                docker inspect \
                       --format "{{range .Mounts}}{{if eq .Destination \"$DB_DATADIR\"}}{{.Source}}{{end}}{{end}}" \
                       "$docker_id")
            if [ "$has_volume_mounted" == "$host_db_working_dir" ]; then
                found="$docker_id"
                break
            fi
        done
        if [ -z "$found" ]; then
            err "Please shutdown any other docker using this directory."
            return 1
        fi
        export container_id="$found"
        info "Found docker $docker_id is already running."
    else
        verb "Database is not locked."
        if ! docker_has_image "$DOCKER_BASE_IMAGE"; then
            docker pull "$DOCKER_BASE_IMAGE"
        fi

        docker_opts=
        debug docker network create "$_DB_NAME"
        if ! network_id=$(docker network create "$_DB_NAME"); then
            err "'docker network create $_DB_NAME' failed !"
            _clean_docker "$_DB_NAME" "$container_id"
            rm "$state_tmpdir/${_DB_NAME}.working"
            return 1
        fi
        debug docker run -d \
              --name "$_DB_NAME" \
              $docker_opts \
              --network "$_DB_NAME" \
              -v "$host_db_working_dir:$DB_DATADIR" \
              "$DOCKER_BASE_IMAGE"
        if ! container_id=$(
                docker run -d \
                       --name "$_DB_NAME" \
                       $docker_opts \
                       --network "$_DB_NAME" \
                       -v "$host_db_working_dir:$DB_DATADIR" \
                       "$DOCKER_BASE_IMAGE"
             ); then
            err "'docker run' failed !"
            _clean_docker "$_DB_NAME" "$container_id"
            rm "$state_tmpdir/${_DB_NAME}.working"
            return 1
        fi
        trap_add EXIT,ERR "_clean_docker \"$_DB_NAME\" \"$container_id\""
    fi

    if docker_ip=$(wait_for_docker_ip "$container_id"); then
        IFS=: read DOCKER_NETWORK DOCKER_IP <<<"$docker_ip"
        echo "$docker_ip" > "$_STATE_FILE"
        debug "written '$_STATE_FILE'"
        rm "$state_tmpdir/${_DB_NAME}.working"
        _set_db_params "$DOCKER_IP" "$DOCKER_NETWORK"
        return 0
    else
        errlvl="$?"
        err "Db not found (errlvl: $errlvl). Tail of docker logs follows:"
        docker logs --tail=5 "$container_id" 2>&1 | prefix "  | " >&2
        rm "$state_tmpdir/${_DB_NAME}.working"
        return "$errlvl"
    fi
}
export -f ensure_db_docker_running


## Require to set $db_docker_opts if needed, and $DB_PASSFILE
##
_dcmd() {
    local docker_opts command="$1"
    shift

    debug "Db> $command $@"

    if [ -f "$HOST_DB_PASSFILE" -a "$CLIENT_DB_PASSFILE" ]; then
        verb "Found and using '$HOST_DB_PASSFILE' as '$CLIENT_DB_PASSFILE'."
        docker_opts=("${db_docker_opts[@]}" "-v" "$HOST_DB_PASSFILE:$CLIENT_DB_PASSFILE")
    else
        docker_opts=("${db_docker_opts[@]}")
    fi

    ## XXXX was here: actualy, we need only connection between this version and the client version
    debug docker run -i --rm \
           "${docker_opts[@]}" \
           --entrypoint "$command" "$DOCKER_BASE_IMAGE" "${db_cmd_opts[@]}" "$@"
    docker run -i --rm \
           "${docker_opts[@]}" \
           --entrypoint "$command" "$DOCKER_BASE_IMAGE" "${db_cmd_opts[@]}" "$@"
}
export -f _dcmd


## Executes code through db
dcmd() {
    local fun
    [ "$DB_NAME" ] || print_syntax_error "$FUNCNAME: You must provide \$DB_NAME."
    [ "$DB_DATADIR" ] || print_syntax_error "$FUNCNAME: You must provide \$DB_DATADIR."
    # [ "$DB_PASSFILE" ] || print_syntax_error "$FUNCNAME: You must provide \$DB_PASSFILE."
    [ "$_PID" ] || print_syntax_error "$FUNCNAME: You must provide \$_PID."
    for fun in is_db_locked _set_db_params ddb; do
        [ "$(type -t "$fun")" == "function" ] ||
            print_syntax_error "$FUNCNAME: You must provide function '$fun'."
    done
    ensure_db_docker_running </dev/null || return 1

    _dcmd "$@"
}
export -f dcmd


get_docker_ips() {
    local name="$1" ip format network_id
    if ! docker inspect --format='{{ .NetworkSettings.Networks }}' "$name" >/dev/null 2>&1; then
        echo "default:$(docker inspect --format='{{ .NetworkSettings.IPAdress }}' "$name"  2>/dev/null)"
    else
        format='{{range $name, $conf := .NetworkSettings.Networks}}{{$name}}{{"\x00"}}{{$conf.IPAddress}}{{"\x00"}}{{end}}'
        while read-0 network_id ip; do
            printf "%s:%s\n" "$network_id" "$ip"
        done < <(docker inspect --format="$format" "$name")
    fi
}
export -f get_docker_ips


get_docker_ip() {
    local name="$1"
    get_docker_ips "$name"
}
export -f get_docker_ip


wait_docker_ip() {
    local name="$1" timeout="${2:-15}" timeout_count=0 docker_ip=
    start=$SECONDS
    while [ -z "$docker_ip" ]; do
        sleep 0.5
        docker_ip=$(get_docker_ip "$name") && break
        elapsed=$((SECONDS - start))
        if ((elapsed > timeout)); then
            err "${RED}timeout error${NORMAL}(${timeout}s):" \
                "Could not find '$name' docker container's IP."
            return 1
        fi
        [ "$elapsed" == "$old_elapsed" ] ||
            verb "Waiting for docker $name... ($elapsed/$timeout)"
        old_elapsed="$elapsed"
    done
    verb "Found docker $name network and IP: $docker_ip"
    echo "$docker_ip"
}
export -f wait_docker_ip


wait_for_tcp_port() {
    local network=$1 host_port=$2 timeout=20
    verb "Trying to connect to $host_port"
    bash_image=${DEFAULT_BASH_IMAGE:-docker.0k.io/bash}
    echo docker run --rm -i --network "$network" "$bash_image" >&2
    docker run --rm -i --network "$network" "$bash_image" <<EOF
    start=\$SECONDS
    while true; do
        timeout 1 bash -c "</dev/tcp/${host_port/://}" >/dev/null 2>&1 && break
        sleep 0.2
        if [ "\$((SECONDS - start))" -gt "$timeout" ]; then
            exit 1
        fi
    done
    exit 0
EOF
    if [ "$?" != 0 ]; then
        err "${RED}timeout error${NORMAL}(${timeout}s):"\
            "Could not connect to $host_port."
        return 1
    fi
    return 0
}
export -f wait_for_tcp_port


## Warning: requires a ``ddb`` matching current database to be checked
wait_for_docker_ip() {
    local name=$1 DOCKER_IP= DOCKER_NETWORK= docker_ips= docker_ip= elapsed timeout=10
    docker_ip=$(wait_docker_ip "$name" 5) || return 1
    IFS=: read DOCKER_NETWORK DOCKER_IP <<<"$docker_ip"
    if ! str_is_ipv4 "$DOCKER_IP"; then
        err "internal 'wait_docker_ip' did not return a valid IP. Returned IP is '$DOCKER_IP'."
        return 1
    fi
    _set_db_params "$DOCKER_IP" "$DOCKER_NETWORK"
    while read-0 port; do
        IFS="/" read port type <<<"$port"
        [ "$type" == "tcp" ] || continue
        wait_for_tcp_port "$DOCKER_NETWORK" "$DOCKER_IP:${port}" || return 17
        info "Host/Port $DOCKER_IP:${port} checked ${GREEN}open${NORMAL}."
    done < <(image_exposed_ports_0 "$container_id")

    ## Checking direct connection
    timeout=30
    start=$SECONDS
    while true; do
        if err=$(echo "SELECT 1;" | ddb 2>&1 >/dev/null); then
            break
        fi
        if ! [[ "$err" == *"the database system is starting up" ]]; then
            err "${RED}db connection error${NORMAL}:" \
                "Could not connect to db on $DOCKER_IP container's IP."
            echo "  Note: IP up, TCP ports is(are) open" >&2
            if [ "$err" ]; then
                echo "  Error:" >&2
                printf "%s\n" "$err" | prefix "   ${RED}!${NORMAL} " >&2
            fi
            return 18
        fi
        debug "Got 'database system is starting up' error."
        elapsed=$((SECONDS - start))
        if ((elapsed > timeout)); then
            err "${RED}db connection error${NORMAL}:"\
            "Could not connect to db on $DOCKER_IP" \
            "container's IP. (IP up, TCP ports is(are) open, sql answer after ${timeout}s)"
            return 1
        fi
        sleep 0.2
    done
    echo "${DOCKER_NETWORK}:${DOCKER_IP}"
    return 0
}
export -f wait_for_docker_ip


docker_add_host_declaration() {
    local src_docker=$1 domain=$2 dst_docker=$3 dst_docker_ip= dst_docker_network
    dst_docker_ip=$(wait_docker_ip "$dst_docker") || exit 1

    IFS=: read dst_docker_ip dst_docker_network <<<"$dst_docker_ip"

    docker exec -i "$src_docker" bash <<EOF
        if cat /etc/hosts | grep -E "^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\s+$domain\$" > /dev/null 2>&1; then
            sed -ri "s/^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\s+$domain\$/$dst_docker_ip $domain/g" /etc/hosts
        else
            echo "$dst_docker_ip $domain" >> /etc/hosts
        fi
EOF
}
export -f docker_add_host_declaration


get_running_containers_for_service() {
    local service="$1"

    docker ps --filter label="compose.service=$service" --format="{{.ID}}"
}
export -f get_running_containers_for_service


get_container_network_ips() {
    local container="$1"
    docker inspect "$container" \
           --format='{{range $key, $val :=.NetworkSettings.Networks}}{{$key}}{{"\x00"}}{{$val.IPAddress}}{{"\x00"}}{{end}}'
}
export -f get_container_network_ips


get_container_network_ip() {
    local container="$1"
    while read-0 network ip; do
        printf "%s\0" "$network" "$ip"
        break
    done < <(get_container_network_ips "$container")
}
export -f get_container_network_ip


##
## Internal Process
##


get_docker_compose_links() {
    local service="$1" cache_file="$state_tmpdir/$FUNCNAME.cache.$1" \
          deps master_service master_target_service _relation_name \
          target_service _relation_config tech_dep
    if [ -z "$service" ]; then
        print_syntax_error "$FUNCNAME: Please specify a service as first argument."
        return 1
    fi

    if [ -e "$cache_file" ]; then
        # debug "$FUNCNAME: cache hit ($*)"
        cat "$cache_file"
        return 0
    fi

    master_service=$(get_top_master_service_for_service "$service") || return 1
    deps=()
    while read-0 _relation_name target_service _relation_config tech_dep; do
        master_target_service="$(get_top_master_service_for_service "$target_service")" || return 1
        [ "$master_service" == "$master_target_service" ] && continue
        type="$(get_service_type "$target_service")" || return 1
        [ "$type" == "run-once" ] && continue
        if [ "$tech_dep" == "reversed" ]; then
            deps+=("$(echo -en "$master_target_service:\n  links:\n    - $master_service")")
        elif [ "$tech_dep" == "True" ]; then
            deps+=("$(echo -en "$master_service:\n  links:\n    - $master_target_service")")
        fi
        ## XXXvlab: an attempt to add depends_on, but this doesn't work well actually
        ## as there's a circular dependency issue. We don't really want the full feature
        ## of depends_on, but just to add it as targets when doing an 'up'
        # deps+=("$(echo -en "$master_service:\n  depends_on:\n    - $master_target_service")")
    done < <(get_service_relations "$service")
    merge_yaml_str "${deps[@]}" | tee "$cache_file" || return 1
    if [ "${PIPESTATUS[0]}" != 0 ]; then
        rm "$cache_file"
        err "Failed to merge YAML from all ${WHITE}links${NORMAL} dependencies."
        return 1
    fi
}


_get_docker_compose_opts() {
    local service="$1" cache_file="$state_tmpdir/$FUNCNAME.cache.$1" \
          compose_def master_service docker_compose_opts
    if [ -z "$service" ]; then
        print_syntax_error "$FUNCNAME: Please specify a service as first argument."
        return 1
    fi

    if [ -e "$cache_file" ]; then
        # debug "$FUNCNAME: cache hit ($*)"
        cat "$cache_file"
        return 0
    fi

    compose_def="$(get_compose_service_def "$service")" || return 1
    master_service="$(get_top_master_service_for_service "$service")"

    if docker_compose_opts=$(echo "$compose_def" | shyaml get-value -y "docker-compose" 2>/dev/null); then
        yaml_key_val_str "$master_service" "$docker_compose_opts"
    fi | tee "$cache_file"
    if [ "${PIPESTATUS[0]}" != 0 ]; then
        rm "$cache_file"
        return 1
    fi
}


##
## By Reading the metadata.yml, we create a docker-compose.yml mixin.
## Some metadata.yml (of subordinates) will indeed modify other
## services than themselves.
_get_docker_compose_service_mixin() {
    local service="$1" cache_file="$state_tmpdir/$FUNCNAME.cache.$1" \
          links_yaml base_mixin links_yaml docker_compose_options \
          charm charm_part
    if [ -z "$service" ]; then
        print_syntax_error "$FUNCNAME: Please specify a service as first argument."
        return 1
    fi

    if [ -e "$cache_file" ]; then
        # debug "$FUNCNAME: cache hit ($*)"
        cat "$cache_file"
        return 0
    fi
    master_service=$(get_top_master_service_for_service "$service") || {
        err "Failed to get top master service for service $DARKYELLOW$service$NORMAL"
        return 1
    }

    ## The compose part

    base_mixin="$master_service:
  labels:
   - compose.service=$service
   - compose.master-service=${master_service}
   - compose.project=$(get_default_project_name)"
    links_yaml=$(get_docker_compose_links "$service") || return 1
    docker_compose_options=$(_get_docker_compose_opts "$service") || return 1

    ## the charm part

    charm_part=$(get_docker_compose_mixin_from_metadata "$service") || return 1

    ## Merge results
    if [ "$charm_part" ]; then
        charm_yaml="$(yaml_key_val_str "$master_service" "$charm_part")" || return 1
        merge_yaml_str "$base_mixin" "$links_yaml" "$charm_yaml" "$docker_compose_options" || return 1
    else
        merge_yaml_str "$base_mixin" "$links_yaml" "$docker_compose_options" || return 1
    fi | tee "$cache_file"
    if [ "${PIPESTATUS[0]}" != 0 ]; then
        err "Failed to constitute the base YAML for service '${DARKYELLOW}$service${NORMAL}'"
        rm "$cache_file"
        return 1
    fi

}
export -f _get_docker_compose_service_mixin


##
## Get full `docker-compose.yml` format for all listed services (and
## their deps)
##

## @export
## @cache: !system !nofail +stdout
get_docker_compose () {
    local cache_file="$state_tmpdir/$FUNCNAME.cache.$(echo "$*" | md5_compat)" \
          entries services service start docker_compose_services
    if [ -e "$cache_file" ]; then
        # debug "$FUNCNAME: cache hit ($*)"
        cat "$cache_file"
        return 0
    fi

    ##
    ## Adding sub services configurations
    ##

    declare -A entries
    start_compilation=$SECONDS
    debug "Compiling 'docker-compose.yml' base for ${DARKYELLOW}$*$NORMAL..."
    for target_service in "$@"; do
        start=$SECONDS
        services=$(get_ordered_service_dependencies "$target_service") || {
            err "Failed to get dependencies for $DARKYELLOW$target_service$NORMAL"
            return 1
        }
        debug "  $DARKYELLOW$target_service$NORMAL deps:$DARKYELLOW" $services "$NORMAL$GRAY(in $((SECONDS - start))s)$NORMAL"
        for service in $services; do

            if [ "${entries[$service]}" ]; then
                ## Prevent double inclusion of same service if this
                ## service is deps of two or more of your
                ## requirements.
                continue
            fi

            ## mark the service as "loaded" as well as it's containers
            ## if this is a subordinate service
            start_service=$SECONDS
            entries[$service]=$(_get_docker_compose_service_mixin "$service") || {
                err "Failed to get service mixin for $DARKYELLOW$service$NORMAL"
                return 1
            }
            debug "    Applied $DARKYELLOW$service$NORMAL charm metadata mixins $GRAY(in $((SECONDS - start_service))s)$NORMAL"
        done
        debug "    ..finished all mixins for $DARKYELLOW$target_service$NORMAL $GRAY(in $((SECONDS - start))s)$NORMAL"
    done

    docker_compose_services=$(merge_yaml_str "${entries[@]}") || {
        err "Failed to merge YAML services entries together."
        return 1
    }

    base_v2="version: '2.0'"
    merge_yaml_str "$(yaml_key_val_str "services" "$docker_compose_services")" \
                   "$base_v2" > "$cache_file" || return 1

    export _current_docker_compose="$(cat "$cache_file")"
    echo "$_current_docker_compose"
    debug "  ..compilation of base 'docker-compose.yml' done $GRAY(in $((SECONDS - start_compilation))s)$NORMAL" || true
    # debug " ** ${WHITE}docker-compose.yml${NORMAL}:"
    # debug "$_current_docker_compose"
}
export -f get_docker_compose


_get_compose_service_def_cached () {
    local service="$1" docker_compose="$2" cache_file="$CACHEDIR/$FUNCNAME.cache.$(echo "$*" | md5_compat)"
    if [ -e "$cache_file" ]; then
        #debug "$FUNCNAME: STATIC cache hit"
        cat "$cache_file" &&
        touch "$cache_file" || return 1
        return 0
    fi
    value=$(echo "$docker_compose" | shyaml get-value "${service//./\\.}" 2>/dev/null)
    [ "$value" == None ] && value=""
    if ! echo "$value" | shyaml get-value "charm" >/dev/null 2>&1; then
        if charm.exists "$service"; then
            value=$(merge_yaml <(echo "charm: $service") <(echo "$value")) || {
                err "Can't merge YAML infered 'charm: $service' with base ${DARKYELLOW}$service${NORMAL} YAML definition."
                return 1
            }
        else
            err "No ${WHITE}charm${NORMAL} value for service $DARKYELLOW$service$NORMAL" \
                "in compose, nor same name charm found."
            return 1
        fi
    fi
    echo "$value" | tee "$cache_file" || return 1
    # if [ "${PIPESTATUS[0]}" != 0 ]; then
    #     rm "$cache_file"
    #     return 1
    # fi
    return 0
    # if [ "${PIPESTATUS[0]}" != 0 -o \! -s "$cache_file" ]; then
    #     rm "$cache_file"
    #     err "PAS OK $service:  $value"
    #     return 1
    # fi
}
export -f _get_compose_service_def_cached


## XXXvlab: a lot to be done to cache the results
get_compose_service_def () {
    local service="$1" docker_compose cache_file="$state_tmpdir/$FUNCNAME.cache.$1" \
          result
    if [ -e "$cache_file" ]; then
        #debug "$FUNCNAME: SESSION cache hit"
        cat "$cache_file" || return 1
        return 0
    fi

    [ -z "$service" ] && print_syntax_error "Missing service as first argument."
    docker_compose=$(get_compose_yml_content) || return 1
    result=$(_get_compose_service_def_cached "$service" "$docker_compose") || return 1
    charm=$(echo "$result" | shyaml get-value charm 2>/dev/null) || return 1
    metadata=$(charm.metadata "$charm") || return 1
    if default_options=$(printf "%s" "$metadata" | shyaml -y -q get-value default-options); then
        default_options=$(yaml_key_val_str "options" "$default_options") || return 1
        result=$(merge_yaml_str "$default_options" "$result") || return 1
    fi
    echo "$result" | tee "$cache_file" || return 1
}
export -f get_compose_service_def


_get_service_charm_cached () {
    local service="$1" service_def="$2" cache_file="$CACHEDIR/$FUNCNAME.cache.$(echo "$*" | md5_compat)"
    if [ -e "$cache_file" ]; then
        # debug "$FUNCNAME: cache hit $1"
        cat "$cache_file" &&
        touch "$cache_file" || return 1
        return 0
    fi
    charm=$(echo "$service_def" | shyaml get-value charm 2>/dev/null)
    if [ -z "$charm" ]; then
        err "Missing ${WHITE}charm${NORMAL} value in service $DARKYELLOW$service$NORMAL definition."
        return 1
    fi
    echo "$charm" | tee "$cache_file" || return 1
}
export -f _get_service_charm_cached


get_service_charm () {
    local service="$1"
    if [ -z "$service" ]; then
        echo ${FUNCNAME[@]} >&2
        print_syntax_error "$FUNCNAME: Please specify a service as first argument."
        return 1
    fi
    service_def=$(get_compose_service_def "$service") || return 1
    _get_service_charm_cached "$service" "$service_def"
}
export -f get_service_charm


## built above the docker-compose abstraction, so it relies on the
## full docker-compose.yml to be already built.
get_service_def () {
    local service="$1" def
    if [ -z "$_current_docker_compose" ]; then
        print_syntax_error "$FUNCNAME is meant to be called after"\
                           "\$_current_docker_compose has been calculated."
    fi

    def=$(echo "$_current_docker_compose" | shyaml get-value "services.${service//./\\.}" 2>/dev/null)
    if [ -z "$def" ]; then
        err "No definition for service $DARKYELLOW$service$NORMAL in compiled 'docker-compose.yml'."
        return 1
    fi
    echo "$def"
}
export -f get_service_def


## Return the base docker image name of a service
service_base_docker_image() {
    local service="$1" cache_file="$state_tmpdir/$FUNCNAME.cache.$1" \
          master_service service_def service_image service_build service_dockerfile
    if [ -e "$cache_file" ]; then
        # debug "$FUNCNAME: cache hit ($*)"
        cat "$cache_file"
        return 0
    fi
    master_service="$(get_top_master_service_for_service "$service")" || {
        err "Could not compute master service for service $DARKYELLOW$service$NORMAL."
        return 1
    }
    service_def="$(get_service_def "$master_service")" || {
        err "Could not get docker-compose service definition for $DARKYELLOW$master_service$NORMAL."
        return 1
    }
    service_image=$(echo "$service_def" | shyaml get-value image 2>/dev/null)
    if [ "$?" != 0 ]; then
        ## According to https://stackoverflow.com/questions/32230577 , if there's a build,
        ## then the builded image will get name ${project}_${service}
        project=$(get_default_project_name) || return 1
        image_name="${project}_${service}"
        if ! docker_has_image "$image_name"; then
            service_build=$(echo "$service_def" | shyaml get-value build 2>/dev/null)
            if [ "$?" != 0 ]; then
                err "Service $DARKYELLOW$service$NORMAL has no ${WHITE}image${NORMAL} nor ${WHITE}build${NORMAL} parameter."
                echo "$service_def" >&2
                return 1
            fi

            docker build "$service_build" -t "${project}_${service}" >&2 || {
                err "Failed to build image for ${DARKYELLOW}$service${NORMAL}."
                return 1
            }
        fi
        printf "%s" "${project}_${service}"
    else
        printf "%s" "${service_image}"
    fi | tee "$cache_file"
    if [ "${PIPESTATUS[0]}" != 0 ]; then
        rm "$cache_file"
        return 1
    fi

}
export -f service_base_docker_image


get_charm_relation_def () {
    local charm="$1" relation_name="$2" cache_file="$state_tmpdir/$FUNCNAME.cache.$1.$2" \
          relation_def metadata
    if [ -e "$cache_file" ]; then
        # debug "$FUNCNAME: cache hit ($*)"
        cat "$cache_file"
        return 0
    fi
    metadata="$(charm.metadata "$charm")" || return 1
    relation_def="$(echo "$metadata" | shyaml get-value "provides.${relation_name}" 2>/dev/null)"
    echo "$relation_def" | tee "$cache_file"
}
export -f get_charm_relation_def


get_charm_tech_dep_orientation_for_relation() {
    local charm="$1" relation_name="$2" cache_file="$state_tmpdir/$FUNCNAME.cache.$1.$2" \
          relation_def value
    if [ -e "$cache_file" ]; then
        # debug "$FUNCNAME: cache hit ($*)"
        cat "$cache_file"
        return 0
    fi
    relation_def=$(get_charm_relation_def "$charm" "$relation_name" 2>/dev/null)
    value=$(echo "$relation_def" | shyaml get-value 'tech-dep' 2>/dev/null)
    value=${value:-True}
    printf "%s" "$value" | tee "$cache_file"
}
export -f get_charm_tech_dep_orientation_for_relation


get_service_relation_tech_dep() {
    local service="$1" relation_name="$2" cache_file="$state_tmpdir/$FUNCNAME.cache.$1.$2" \
          charm tech_dep
    if [ -e "$cache_file" ]; then
        # debug "$FUNCNAME: cache hit ($*)"
        cat "$cache_file"
        return 0
    fi
    charm=$(get_service_charm "$service") || return 1
    tech_dep="$(get_charm_tech_dep_orientation_for_relation "$charm" "$relation_name")" || return 1
    printf "%s" "$tech_dep" | tee "$cache_file"
}
export -f get_service_relation_tech_dep


##
## Use compose file to get deps, and relation definition in metadata.yml
## for tech-dep attribute.
get_service_deps() {
    local service="$1" cache_file="$state_tmpdir/$FUNCNAME.cache.$1"
    if [ -e "$cache_file" ]; then
        # debug "$FUNCNAME: cache hit ($*)"
        cat "$cache_file"
        return 0
    fi
    (
        set -o pipefail
        get_service_relations "$service" | \
            while read-0 relation_name target_service _relation_config tech_dep; do
                echo "$target_service"
            done | tee "$cache_file"
    ) || return 1
}
export -f get_service_deps


_rec_get_depth() {
    local elt=$1 dep deps max cache_file="$state_tmpdir/$FUNCNAME.cache.$1"
    [ "${depths[$elt]}" ] && return 0
    if [ -e "$cache_file" ]; then
        # debug "$FUNCNAME: cache hit ($*)"
        depths[$elt]=$(cat "$cache_file")
        return 0
    fi

    visited[$elt]=1
    #debug "Setting visited[$elt]"
    #debug "Asking for $DARKYELLOW$elt$NORMAL dependencies"
    deps=$(get_service_deps "$elt") || {
        debug "Failed get_service_deps $elt"
        return 1
    }
    # debug "$elt deps are:" $deps
    max=0
    for dep in $deps; do
        [ "${visited[$dep]}" ] && {
            #debug "Already computing $dep"
            continue
        }
        _rec_get_depth "$dep" || return 1
        #debug "Requesting depth[$dep]"
        if (( ${depths[$dep]} > max )); then
            max="${depths[$dep]}"
        fi
    done
    # debug "Setting depth[$elt] to $((max + 1))"
    depths[$elt]=$((max + 1))
    echo "${depths[$elt]}" > $cache_file
}
export -f _rec_get_depth


get_ordered_service_dependencies() {
    local services=("$@") cache_file="$state_tmpdir/$FUNCNAME.cache.$(echo "$*" | md5_compat)"
    if [ -e "$cache_file" ]; then
        # debug "$FUNCNAME: cache hit ($*)"
        cat "$cache_file"
        return 0
    fi

    #debug "Figuring ordered deps of $DARKYELLOW$services$NORMAL"
    if [ -z "${services[*]}" ]; then
        return 0
        # print_syntax_error "$FUNCNAME: no arguments"
        # return 1
    fi

    declare -A depths
    declare -A visited
    heads=("${services[@]}")
    while [ "${#heads[@]}" != 0 ]; do
        array_pop heads head
        _rec_get_depth "$head" || return 1
    done

    i=0
    while [ "${#depths[@]}" != 0 ]; do
        for key in "${!depths[@]}"; do
            value="${depths[$key]}"
            if [ "$value" == "$i" ]; then
                echo "$key"
                unset depths[$key]
            fi
        done
        i=$((i + 1))
    done | tee "$cache_file"
}
export -f get_ordered_service_dependencies


run_service_hook () {
    local action="$1" service subservice subservices loaded
    shift
    declare -A loaded
    for service in "$@"; do
        subservices=$(get_ordered_service_dependencies "$service") || return 1
        for subservice in $subservices; do
            if [ "${loaded[$subservice]}" ]; then
                ## Prevent double inclusion of same service if this
                ## service is deps of two or more of your
                ## requirements.
                continue
            fi

            charm=$(get_service_charm "$subservice") || return 1
            charm.has_hook "$charm" "$action" >/dev/null || continue

            PROJECT_NAME=$(get_default_project_name) || return 1
            MASTER_BASE_SERVICE_NAME=$(get_top_master_service_for_service "$subservice") || return 1
            MASTER_BASE_CHARM_NAME=$(get_service_charm "$MASTER_BASE_SERVICE_NAME") || return 1
            DOCKER_BASE_IMAGE=$(service_base_docker_image "$MASTER_BASE_SERVICE_NAME") || return 1

            Wrap -d "running $YELLOW$action$NORMAL hook of $DARKYELLOW$subservice$NORMAL in charm $DARKPINK$charm$NORMAL" <<EOF || return 1

            export DOCKER_BASE_IMAGE="$DOCKER_BASE_IMAGE"
            export SERVICE_NAME=$subservice
            export IMAGE_NAME=$(echo "${PROJECT_NAME}" | tr -d "_-")_\${SERVICE_NAME}
            export CONTAINER_NAME=\${IMAGE_NAME}_1
            export CHARM_NAME="$charm"
            export PROJECT_NAME="$PROJECT_NAME"
            export SERVICE_DATASTORE="$DATASTORE/$subservice"
            export SERVICE_CONFIGSTORE="$CONFIGSTORE/$subservice"
            export MASTER_BASE_SERVICE_NAME="$MASTER_BASE_SERVICE_NAME"
            export MASTER_BASE_CHARM_NAME="$MASTER_BASE_CHARM_NAME"

            charm.run_hook "$charm" "$action"

EOF
            loaded[$subservice]=1
        done
    done
    return 0
}


host_resource_get() {
    local location="$1" cfg="$2"
    type=$(echo "$cfg" | shyaml get-value type 2>/dev/null) || {
        err "Missing ${WHITE}type$NORMAL option in ${WHITE}get$NORMAL config for location '$location'"
        return 1
    }
    if fn.exists host_resource_get_$type; then
        host_resource_get_$type "$location" "$cfg"
    else
        err "Source ${WHITE}source$NORMAL type '$type' unknown for" \
            "${WHITE}host-resource$NORMAL '$location' defined in" \
            "$DARKYELLOW$subservice$NORMAL config."
        return 1
    fi
}
export -f host_resource_get


host_resource_get_git() {
    local location="$1" cfg="$2" branch parent url
    branch=$(echo "$cfg" | shyaml get-value branch 2>/dev/null)
    branch=${branch:-master}

    url=$(echo "$cfg" | shyaml get-value url 2>/dev/null)
    parent="$(dirname "$location")"
    (
        mkdir -p "$parent" && cd "$parent" &&
            git clone -b "$branch" "$url" "$(basename "$location")"
    ) || return 1
}
export -f host_resource_get_git


host_resource_get_git-sub() {
    local location="$1" cfg="$2" branch parent url
    branch=$(echo "$cfg" | shyaml get-value branch 2>/dev/null)
    branch=${branch:-master}

    url=$(echo "$cfg" | shyaml get-value url 2>/dev/null)
    parent="$(dirname "$location")"
    (
        mkdir -p "$parent" && cd "$parent" &&
            git sub clone -b "$branch" "$url" "$(basename "$location")"
    ) || return 1
}
export -f host_resource_get_git-sub


setup_host_resource () {
    local subservice="$1" service_def location get cfg

    service_def=$(get_compose_service_def "$subservice") || return 1
    while read-0 location cfg; do
        ## XXXvlab: will it be a git resources always ?
        if [ -d "$location" -a ! -d "$location/.git" ]; then
            err "Hum, location '$location' does not seem to be a git directory."
            return 1
        fi
        if [ -d "$location" ]; then
            info "host resource '$location' already set up."
            continue
        fi
        get=$(echo "$cfg" | shyaml get-value get 2>/dev/null)

        if [ -z "$get" ]; then
            err "No host directory '$location' found, and no ${WHITE}source$NORMAL" \
                "specified for $DARKYELLOW$subservice$NORMAL."
            return 1
        fi
        host_resource_get "$location" "$get" || return 1
    done < <(echo "$service_def" | shyaml key-values-0 host-resources 2>/dev/null)
}
export -f setup_host_resource


setup_host_resources () {
    local service subservices subservice loaded

    declare -A loaded
    for service in "$@"; do
       subservices=$(get_ordered_service_dependencies "$service") || return 1
        for subservice in $subservices; do
            if [ "${loaded[$subservice]}" ]; then
                ## Prevent double inclusion of same service if this
                ## service is deps of two or more of your
                ## requirements.
                continue
            fi
            setup_host_resource "$service"
            loaded[$subservice]=1
        done
    done
    return 0
}
export -f setup_host_resources


relation-get () {
    local key="$1" out
    if ! out=$(cat "$RELATION_DATA_FILE" | shyaml -y get-value "$key" 2>/dev/null); then
        err "The key $WHITE$key$NORMAL was not found in relation's data."
        return 1
    fi
    echo "$out" | yaml_get_interpret
}
export -f relation-get


expand_vars() {
    local unlikely_prefix="UNLIKELY_PREFIX"
    content=$(cat -)
    ## find first identifier not in content
    remaining_lines=$(echo "$content" | grep "^$unlikely_prefix")
    while [ "$(echo "$remaining_lines" | grep "^$unlikely_prefix$")" ]; do
        size_prefix="${#unlikely_prefix}"
        first_matching=$(echo "$remaining_lines" |
                             grep -v "^$unlikely_prefix$" |
                             uniq -w "$((size_prefix + 1))" -c |
                             sort -rn |
                             head -n 1)
        first_matching=${first_matching#"${x%%[![:space:]]*}"}
        first_matching="${first_matching#* }"
        next_char=${first_matching:$size_prefix:1}
        if [ "$next_char" != "0" ]; then
            unlikely_prefix+="0"
        else
            unlikely_prefix+="1"
        fi
        remaining_lines=$(echo "$remaining_lines" | grep "^$unlikely_prefix")
    done
    eval "cat <<$unlikely_prefix
$content
$unlikely_prefix"
}
export -f expand_vars


yaml_get_interpret() {
    local content tag
    content=$(cat -)
    tag=$(echo "$content" | shyaml -y get-value) || return 1
    tag="${tag%% *}"
    content=$(echo "$content" | shyaml get-value) || return 1
    if ! [ "${tag:0:1}" == "!" ]; then
        echo "$content" || return 1
        return 0
    fi
    case "$tag" in
        "!bash-stdout")
            echo "$content" | bash || {
                err "shell code didn't end with errorlevel 0"
                return 1
            }
            ;;
        "!var-expand")
            echo "$content" | expand_vars || {
                err "shell expansion failed"
                return 1
            }
            ;;
        *)
            err "Invalid object tag ${WHITE}$tag${NORMAL}"
            return 1
            ;;
    esac
}
export -f yaml_get_interpret


options-get () {
    local key="$1" out
    service_def=$(get_compose_service_def "$SERVICE_NAME") || return 1
    if ! out=$(echo "$service_def" | shyaml -y get-value "options.$key" 2>/dev/null); then
        err "The key $WHITE$key$NORMAL was not found in base service compose definition.."
        return 1
    fi
    echo "$out" | yaml_get_interpret
}
export -f options-get


relation-base-compose-get () {
    local key="$1" out
    if ! out=$(echo "$RELATION_BASE_COMPOSE_DEF" | shyaml -y get-value "options.$key" 2>/dev/null); then
        err "The key $WHITE$key$NORMAL was not found in base service compose definition.."
        return 1
    fi
    echo "$out" | yaml_get_interpret
}
export -f relation-base-compose-get


relation-target-compose-get () {
    local key="$1" out
    if ! out=$(echo "$RELATION_TARGET_COMPOSE_DEF" | shyaml -y get-value "options.$key" 2>/dev/null); then
        err "The key $WHITE$key$NORMAL was not found in base service compose definition.."
        return 1
    fi
    echo "$out" | yaml_get_interpret
}
export -f relation-target-compose-get


relation-set () {
    local key="$1" value="$2"
    if [ -z "$RELATION_DATA_FILE" ]; then
        err "$FUNCNAME: relation does not seems to be correctly setup."
        return 1
    fi

    if ! [ -r "$RELATION_DATA_FILE" ]; then
        err "$FUNCNAME: can't read relation's data." >&2
        return 1
    fi

    _config_merge "$RELATION_DATA_FILE" <(yaml_key_val_str "$key" "$value")
}
export -f relation-set


_config_merge() {
    local config_filename="$1" merge_to_file="$2"
    touch "$config_filename" &&
    merge_yaml "$config_filename" "$merge_to_file" > "$config_filename.tmp" || return 1
    mv "$config_filename.tmp" "$config_filename"
}
export -f _config_merge


## XXXvlab; this can be used only in relation, I'd like to use it in init.
config-add() {
    local metadata="$1"
    _config_merge "$RELATION_CONFIG" <(echo "$metadata")
}
export -f config-add


## XXXvlab; this can be used only in relation, I'd like to use it in init.
init-config-add() {
    local metadata="$1"
    _config_merge "$state_tmpdir/to-merge-in-docker-compose.yml" \
                  <(yaml_key_val_str "services" "$metadata")
}
export -f init-config-add


docker_get_uid() {
    local service="$1" user="$2" uid
    uid=$(cached_cmd_on_base_image "$service" "id -u \"$user\"") || {
        debug "Failed to query for '$user' uid in ${DARKYELLOW}$service${NORMAL} base image."
        return 1
    }
    info "uid from ${DARKYELLOW}$service${NORMAL} for user '$user' is '$uid'"
    echo "$uid"
}
export -f docker_get_uid


logstdout() {
    local name="$1"
    sed -r 's%^%'"${name}"'> %g'
}
export -f logstdout


logstderr() {
    local name="$1"
    sed -r 's%^(.*)$%'"${RED}${name}>${NORMAL} \1"'%g'
}
export -f logstderr


_run_service_relation () {
    local relation_name="$1" service="$2" target_service="$3" relation_config="$4" relation_dir services

    charm=$(get_service_charm "$service") || return 1
    target_charm=$(get_service_charm "$target_service") || return 1

    base_script_name=$(charm.has_relation_hook "$charm" "$relation_name" relation-joined) || true
    target_script_name=$(charm.has_relation_hook "$target_charm" "$relation_name" relation-joined) || true
    [ "$base_script_name" -o "$target_script_name" ] || return 0

    relation_dir=$(get_relation_data_dir "$service" "$target_service" "$relation_name") || return 1
    RELATION_DATA_FILE=$(get_relation_data_file "$service" "$target_service" "$relation_name" "$relation_config") || return 1
    export BASE_SERVICE_NAME=$service
    export BASE_CHARM_NAME=$charm
    export BASE_CHARM_PATH=$(charm.get_dir "$charm")
    export TARGET_SERVICE_NAME=$target_service
    export TARGET_CHARM_NAME=$target_charm
    export TARGET_CHARM_PATH=$(charm.get_dir "$target_charm")
    export RELATION_DATA_FILE
    target_errlvl=0

    if [ -z "$target_script_name" ]; then
        verb "No relation script $DARKBLUE$relation_name$NORMAL in target $DARKPINK$target_charm$NORMAL."
    else
        verb "Running ${DARKBLUE}$relation_name${NORMAL} relation-joined script" \
             "for target $DARKYELLOW$target_service$NORMAL (charm $DARKPINK$target_charm$NORMAL)"
        RELATION_CONFIG="$relation_dir/config_provider"
        DOCKER_BASE_IMAGE=$(service_base_docker_image "$target_service") || return 1
        export DOCKER_BASE_IMAGE RELATION_CONFIG RELATION_DATA
        {
            (
                SERVICE_NAME=$target_service
                SERVICE_DATASTORE="$DATASTORE/$target_service"
                SERVICE_CONFIGSTORE="$CONFIGSTORE/$target_service"
                export SERVICE_NAME DOCKER_BASE_IMAGE SERVICE_DATASTORE SERVICE_CONFIGSTORE
                charm.run_relation_hook "$target_charm" "$relation_name" relation-joined
                echo "$?" > "$relation_dir/target_errlvl"
            ) | logstdout "$DARKYELLOW$target_service$NORMAL/$DARKBLUE$relation_name$NORMAL (joined) ${GREEN}@${NORMAL}"
        } 3>&1 1>&2 2>&3 | logstderr "$DARKYELLOW$target_service$NORMAL/$DARKBLUE$relation_name$NORMAL (joined) ${RED}@${NORMAL}" 3>&1 1>&2 2>&3
        target_errlvl="$(cat "$relation_dir/target_errlvl")" || {
            err "Relation script '$script_name' in $DARKPINK$target_charm$NORMAL" \
                "failed before outputing an errorlevel."
            ((target_errlvl |= "1" ))
        }
        if [ -e "$RELATION_CONFIG" ]; then
            debug "Merging some new config info in $DARKYELLOW$target_service$NORMAL"
            _config_merge "$state_tmpdir/to-merge-in-docker-compose.yml" "$RELATION_CONFIG" &&
                rm "$RELATION_CONFIG"
            ((target_errlvl |= "$?"))
        fi
    fi

    if [ "$target_errlvl" == 0 ]; then
        errlvl=0
        if [ "$base_script_name" ]; then
            verb "Running ${DARKBLUE}$relation_name${NORMAL} relation-joined script" \
                 "for $DARKYELLOW$service$NORMAL (charm $DARKPINK$charm$NORMAL)"
            RELATION_CONFIG="$relation_dir/config_providee"
            RELATION_DATA="$(cat "$RELATION_DATA_FILE")"
            DOCKER_BASE_IMAGE=$(service_base_docker_image "$service") || return 1
            export DOCKER_BASE_IMAGE RELATION_CONFIG RELATION_DATA
            {
                (
                    SERVICE_NAME=$service
                    SERVICE_DATASTORE="$DATASTORE/$service"
                    SERVICE_CONFIGSTORE="$CONFIGSTORE/$service"
                    export SERVICE_NAME DOCKER_BASE_IMAGE SERVICE_DATASTORE SERVICE_CONFIGSTORE
                    charm.run_relation_hook "$charm" "$relation_name" relation-joined
                    echo "$?" > "$relation_dir/errlvl"
                ) | logstdout "$DARKYELLOW$service$NORMAL/$DARKBLUE$relation_name$NORMAL (joined) ${GREEN}@${NORMAL}"
            } 3>&1 1>&2 2>&3 | logstderr "$DARKYELLOW$service$NORMAL/$DARKBLUE$relation_name$NORMAL (joined) ${RED}@$NORMAL" 3>&1 1>&2 2>&3
            errlvl="$(cat "$relation_dir/errlvl")" || {
                err "Relation script '$script_name' in $DARKPINK$charm$NORMAL" \
                    "failed before outputing an errorlevel."
                ((errlvl |= "1" ))
            }
            if [ -e "$RELATION_CONFIG" ]; then
                _config_merge "$state_tmpdir/to-merge-in-docker-compose.yml" "$RELATION_CONFIG" &&
                    rm "$RELATION_CONFIG"
                ((errlvl |= "$?" ))
            fi
            if [ "$errlvl" != 0 ]; then
                err "Relation $DARKBLUE$relation_name$NORMAL on $DARKYELLOW$service$NORMAL failed to run properly."
            fi
        else
            verb "No relation script '$script_name' in charm $DARKPINK$charm$NORMAL. Ignoring."
        fi
    else
        err "Relation $DARKBLUE$relation_name$NORMAL on $DARKYELLOW$target_service$NORMAL failed to run properly."
    fi

    if [ "$target_errlvl" == 0 -a "$errlvl" == 0 ]; then
        debug "Relation $DARKBLUE$relation_name$NORMAL is established" \
              "between $DARKYELLOW$service$NORMAL and $DARKYELLOW$target_service$NORMAL."
        return 0
    else
        return 1
    fi
}
export -f _run_service_relation


_get_compose_relations_cached () {
    local compose_service_def="$1" cache_file="$CACHEDIR/$FUNCNAME.cache.$(echo "$*" | md5_compat)" \
          relation_name relation_def target_service
    if [ -e "$cache_file" ]; then
        #debug "$FUNCNAME: STATIC cache hit $1"
        cat "$cache_file" &&
        touch "$cache_file" || return 1
        return 0
    fi

    (
        set -o pipefail
        if [ "$compose_service_def" ]; then
            while read-0 relation_name relation_def; do
                ## XXXvlab: could we use braces here instead of parenthesis ?
                (
                    case "$(echo "$relation_def" | shyaml get-type 2>/dev/null)" in
                    "str")
                        target_service="$(echo "$relation_def" | shyaml get-value 2>/dev/null)" || return 1
                        tech_dep="$(get_service_relation_tech_dep "$target_service" "$relation_name")" || return 1
                        printf "%s\0" "$relation_name" "$target_service" "" "$tech_dep"
                        ;;
                    "sequence")
                        while read-0 target_service; do
                            tech_dep="$(get_service_relation_tech_dep "$target_service" "$relation_name")" || return 1
                            printf "%s\0" "$relation_name" "$target_service" "" "$tech_dep"
                        done < <(echo "$relation_def" | shyaml get-values-0 2>/dev/null)
                        ;;
                    "struct")
                        while read-0 target_service relation_config; do
                            tech_dep="$(get_service_relation_tech_dep "$target_service" "$relation_name")" || return 1
                            printf "%s\0" "$relation_name" "$target_service" "$relation_config" "$tech_dep"
                        done < <(echo "$relation_def" | shyaml key-values-0 2>/dev/null)
                        ;;
                    esac
                ) </dev/null >> "$cache_file" || return 1
            done < <(echo "$compose_service_def" | shyaml key-values-0 relations 2>/dev/null)
        fi
    )
    if [ "$?" != 0 ]; then
        err "Error while looking for compose relations."
        rm -f "$cache_file"  ## no cache
        return 1
    fi
    [ -e "$cache_file" ] && cat "$cache_file"
    return 0
}
export -f _get_compose_relations_cached


get_compose_relations () {
    local service="$1" cache_file="$state_tmpdir/$FUNCNAME.cache.$1" \
          compose_def

    if [ -e "$cache_file" ]; then
        #debug "$FUNCNAME: SESSION cache hit $1"
        cat "$cache_file"
        return 0
    fi

    compose_def="$(get_compose_service_def "$service")" || return 1
    _get_compose_relations_cached "$compose_def" > "$cache_file"
    if [ "$?" != 0 ]; then
        rm -f "$cache_file"  ## no cache
        return 1
    fi
    cat "$cache_file"
}
export -f get_compose_relations


get_service_relations () {
    local service="$1" cache_file="$state_tmpdir/$FUNCNAME.cache.$1" \
          s rn ts rc td
    if [ -e "$cache_file" ]; then
        #debug "$FUNCNAME: SESSION cache hit $1"
        cat "$cache_file"
        return 0
    fi

    if [ -z "$ALL_RELATIONS" ]; then
        err "Can't access global \$ALL_RELATIONS"
        return 1
    fi

    while read-0 s rn ts rc td; do
        [[ "$s" == "$service" ]] || continue
        printf "%s\0" "$rn" "$ts" "$rc" "$td"
    done < <(cat "$ALL_RELATIONS") > "$cache_file"

    if [ "$?" != 0 ]; then
        rm -f "$cache_file"  ## no cache
        return 1
    fi
    cat "$cache_file"
}
export -f get_service_relations


get_service_relation() {
    local service="$1" relation="$2" cache_file="$state_tmpdir/$FUNCNAME.cache.$1" \
          rn ts rc td
    if [ -e "$cache_file" ]; then
        #debug "$FUNCNAME: SESSION cache hit $1"
        cat "$cache_file"
        return 0
    fi

    while read-0 rn ts rc td; do
        [ "$relation" == "$rn" ] && {
            printf "%s\0" "$ts" "$rc" "$td"
            break
        }
    done < <(get_service_relations "$service") > "$cache_file"
    if [ "$?" != 0 ]; then
        rm -f "$cache_file"  ## no cache
        return 1
    fi
    cat "$cache_file"
}
export -f get_service_relation


_get_charm_metadata_uses() {
    local metadata="$1" cache_file="$CACHEDIR/$FUNCNAME.cache.$(printf "%s\0" "$@" | md5_compat)"
    if [ -e "$cache_file" ]; then
        #debug "$FUNCNAME: SESSION cache hit $1"
        cat "$cache_file" || return 1
        return 0
    fi

    printf "%s" "$metadata" | { shyaml key-values-0 uses 2>/dev/null || true; } | tee "$cache_file"
}
export -f _get_charm_metadata_uses


_get_service_metadata() {
    local service="$1" cache_file="$state_tmpdir/$FUNCNAME.cache.$1" \
          charm

    if [ -e "$cache_file" ]; then
        #debug "$FUNCNAME: SESSION cache hit $1"
        cat "$cache_file"
        return 0
    fi

    charm="$(get_service_charm "$service")" || return 1
    charm.metadata "$charm" > "$cache_file"
    if [ "$?" != 0 ]; then
        rm -f "$cache_file"  ## no cache
        return 1
    fi
    cat "$cache_file"
}
export -f _get_service_metadata


_get_service_uses() {
    local service="$1" cache_file="$state_tmpdir/$FUNCNAME.cache.$1" \
          metadata

    if [ -e "$cache_file" ]; then
        #debug "$FUNCNAME: SESSION cache hit $1"
        cat "$cache_file"
        return 0
    fi

    metadata="$(_get_service_metadata "$service")" || return 1
    _get_charm_metadata_uses "$metadata" > "$cache_file"
    if [ "$?" != 0 ]; then
        rm -f "$cache_file"  ## no cache
        return 1
    fi
    cat "$cache_file"
}
export -f _get_service_uses


_get_services_uses() {
    local cache_file="$state_tmpdir/$FUNCNAME.cache.$(printf "%s\0" "$@" | md5_compat)" \
          service rn rd

    if [ -e "$cache_file" ]; then
        #debug "$FUNCNAME: SESSION cache hit $1"
        cat "$cache_file"
        return 0
    fi

    for service in "$@"; do
        _get_service_uses "$service" | while read-0 rn rd; do
            printf "%s\0" "$service" "$rn" "$rd"
        done
        [ "${PIPESTATUS[0]}" == 0 ] || {
            return 1
        }
    done > "${cache_file}.wip"
    mv "${cache_file}"{.wip,} &&
    cat "$cache_file" || return 1
}
export -f _get_services_uses


_get_provides_provides() {
    local provides="$1" cache_file="$CACHEDIR/$FUNCNAME.cache.$(printf "%s\0" "$@" | md5_compat)" \
          service rn rd

    if [ -e "$cache_file" ]; then
        # debug "$FUNCNAME: CACHEDIR cache hit $1"
        cat "$cache_file"
        return 0
    fi

    type=$(printf "%s" "$provides" | shyaml get-type)
    case "$type" in
        sequence)
            while read-0 prov; do
                printf "%s\0" "$prov" ""
            done < <(echo "$provides" | shyaml get-values-0)
            ;;
        struct)
            printf "%s" "$provides" | shyaml key-values-0
            ;;
        str)
            printf "%s\0" "$(echo "$provides" | shyaml get-value)" ""
            ;;
        *)
            err "Unexpected type '$type' for provider identifier in charm '$charm'."
            return 1
    esac | tee "$cache_file"
    return "${PIPESTATUS[0]}"
}


_get_metadata_provides() {
    local metadata="$1" cache_file="$CACHEDIR/$FUNCNAME.cache.$(printf "%s\0" "$@" | md5_compat)" \
          service rn rd

    if [ -e "$cache_file" ]; then
        #debug "$FUNCNAME: CACHEDIR cache hit"
        cat "$cache_file"
        return 0
    fi

    provides=$(printf "%s" "$metadata" | shyaml get-value -y -q provides "")
    [ "$provides" -a "$provides" != "''" ] || { touch "$cache_file"; return 0; }

    _get_provides_provides "$provides" | tee "$cache_file"
    return "${PIPESTATUS[0]}"
}

_get_services_provides() {
    local cache_file="$state_tmpdir/$FUNCNAME.cache.$(printf "%s\0" "$@" | md5_compat)" \
          service rn rd

    if [ -e "$cache_file" ]; then
        #debug "$FUNCNAME: SESSION cache hit $1"
        cat "$cache_file"
        return 0
    fi

    ## YYY: replace the inner loop by a cached function
    for service in "$@"; do
        metadata="$(_get_service_metadata "$service")" || return 1

        while read-0 rn rd; do
            printf "%s\0" "$service" "$rn" "$rd"
        done < <(_get_metadata_provides "$metadata")
    done > "$cache_file"
    if [ "$?" != 0 ]; then
        rm -f "$cache_file"  ## no cache
        return 1
    fi
    cat "$cache_file"
}
export -f _get_services_provides


_get_charm_provides() {
    local cache_file="$CACHEDIR/$FUNCNAME.cache.$(charm.store_metadata_hash)"
    if [ -e "$cache_file" ]; then
        #debug "$FUNCNAME: SESSION cache hit"
        cat "$cache_file"
        return 0
    fi
    start="$SECONDS"
    debug "Getting charm provider list..."
    while read-0 charm _ realpath metadata; do
        metadata="$(charm.metadata "$charm")" || continue
        # echo "reading $charm" >&2
        while read-0 rn rd; do
            printf "%s\0" "$charm" "$rn" "$rd"
        done < <(_get_metadata_provides "$metadata")
    done < <(charm.ls) | tee "$cache_file"
    errlvl="${PIPESTATUS[0]}"
    debug "  ..charm provider list done $GRAY(in $((SECONDS - start))s)$NORMAL"
    return "$errlvl"
}


_get_charm_providing() {
    local cache_file="$state_tmpdir/$FUNCNAME.cache.$(printf "%s\0" "$@" | md5_compat)" \
          relation="$1"
    if [ -e "$cache_file" ]; then
        #debug "$FUNCNAME: SESSION cache hit $1"
        cat "$cache_file"
        return 0
    fi

    while read-0 charm relation_name relation_def; do
        [ "$relation_name" == "$relation" ] || continue
        printf "%s\0" "$charm" "$relation_def"
    done < <(_get_charm_provides) > "$cache_file"

    if [ "$?" != 0 ]; then
        rm -f "$cache_file"  ## no cache
        return 1
    fi
    cat "$cache_file"
}


_get_services_providing() {
    local cache_file="$state_tmpdir/$FUNCNAME.cache.$(printf "%s\0" "$@" | md5_compat)" \
          relation="$1"
    shift   ## services is "$@"
    if [ -e "$cache_file" ]; then
        #debug "$FUNCNAME: SESSION cache hit $1"
        cat "$cache_file"
        return 0
    fi

    while read-0 service relation_name relation_def; do
        [ "$relation_name" == "$relation" ] || continue
        printf "%s\0" "$service" "$relation_def"
    done < <(_get_services_provides "$@") > "$cache_file"

    if [ "$?" != 0 ]; then
        rm -f "$cache_file"  ## no cache
        return 1
    fi
    cat "$cache_file"
}
export -f _get_services_provides


_out_new_relation_from_defs() {
    local service="$1" rn="$2" ts="$3" prov_def="$4" rel_def="$5" rc td rc_prov

    rc_prov=$(printf "%s" "$prov_def" | shyaml -y get-value "default-options" 2>/dev/null)
    ## YYYvlab: should be seen even in no debug mode no ?
    rc=$(printf "%s" "$rel_def" | shyaml -y get-value "default-options" 2>/dev/null)

    td=$(echo "$prov_def" | shyaml get-value 'tech-dep' 2>/dev/null)
    td=${td:-True}
    rc=$(merge_yaml_str "$rc_prov" "$rc") || return 1

    printf "%s\0" "$service" "$relation_name" "$ts" "$rc" "$td"
}



get_all_relations () {
    local cache_file="$state_tmpdir/$FUNCNAME.cache.$(printf "%s\0" "$@" | md5_compat)" \
          services

    if [ -e "${cache_file}" ]; then
        #debug "$FUNCNAME: SESSION cache hit $1"
        cat "${cache_file}"
        return 0
    fi

    declare -A services
    services_uses=()
    ## XXXvlab: bwerk, leveraging cache to be able to get the errorlevel here.
    _get_services_uses "$@" || return 1
    array_read-0 services_uses < <(_get_services_uses "$@")
    services_provides=()
    ## XXXvlab: bwerk, leveraging cache to be able to get the errorlevel here.
    _get_services_provides "$@" || return 1
    array_read-0 services_provides < <(_get_services_provides "$@")

    for service in "$@"; do
        services[$service]=1
    done

    all_services=("$@")
    while [ "${#all_services[@]}" != 0 ]; do
        array_pop all_services service
        while read-0 relation_name ts relation_config tech_dep; do
            printf "%s\0" "$service" "$relation_name" "$ts" "$relation_config" "$tech_dep"

            ## adding target services ?
            [ "${services[$ts]}" ] && continue
            array_read-0 services_uses < <(_get_services_uses "$ts")
            all_services+=("$ts")
            services[$ts]=1
        done < <(get_compose_relations "$service")
    done > "${cache_file}.wip"

    while true; do
        changed=
        new_services_uses=()
        summon=()
        required=()
        recommended=()
        optional=()
        while [ "${#services_uses[@]}" != 0 ]; do
            service="${services_uses[0]}"
            relation_name="${services_uses[1]}"
            relation_def="${services_uses[2]}"
            services_uses=("${services_uses[@]:3}")

            default_options=$(printf "%s" "$relation_def" | shyaml -y get-value "default-options" 2>/dev/null)
            ## is this "use" declaration satisfied ?
            found=
            while read-0 s rn ts rc td; do
                if [ -z "$found" -a "$service" == "$s" -a "$relation_name" == "$rn" ]; then
                    if [ "$default_options" ]; then
                        rc=$(merge_yaml_str "$default_options" "$rc") || return 1
                    fi
                    found="$ts"
                fi
                printf "%s\0" "$s" "$rn" "$ts" "$rc" "$td"
            done < "${cache_file}.wip" > "${cache_file}.wip.new"
            mv "${cache_file}.wip.new" "${cache_file}.wip"
            if [ "$found" ]; then  ## this "use" declaration was satisfied
                debug "${DARKYELLOW}$service${NORMAL} use declaration for relation " \
                      "${DARKBLUE}$relation_name${NORMAL} is satisfied with ${DARKYELLOW}$found${NORMAL}"
                continue
            fi

            auto=$(echo "$relation_def" | shyaml get-value auto pair 2>/dev/null)
            case "$auto" in
                "pair")
                    service_list=()
                    array_read-0 service_list < <(array_keys_to_stdin services)
                    providers=()
                    array_read-0 providers providers_def < <(_get_services_providing "$relation_name" "${service_list[@]}")
                    if [ "${#providers[@]}" == 1 ]; then
                        ts="${providers[0]}"
                        debug "Auto-pairs ${DARKYELLOW}$service${NORMAL}" \
                              "--${DARKBLUE}$relation_name${NORMAL}--> ${DARKYELLOW}$ts${NORMAL}"

                        _out_new_relation_from_defs "$service" "$relation_name" "$ts" \
                                                    "${providers_def[0]}" "$relation_def" \
                                                    >> "${cache_file}.wip"

                        ## Adding service
                        [ "${services[$ts]}" ] && continue
                        array_read-0 new_services_uses < <(_get_services_uses "$ts")
                        services[$ts]=1
                        changed=1
                        continue
                    elif [ "${#providers[@]}" -gt 1 ]; then
                        msg=""
                        warn "No auto-pairing ${DARKYELLOW}$service${NORMAL}" \
                             "--${DARKBLUE}$relation_name${NORMAL}--> ($DARKYELLOW""${providers[@]}""$NORMAL)"\
                             "(> 1 provider)."
                        continue
                    else
                        :  ## Do nothing
                    fi
                    ;;
                "summon")
                    summon+=("$service" "$relation_name" "$relation_def")
                    ;;
                ""|null|disable|disabled)
                    :
                    ;;
                *)
                    err "Invalid ${WHITE}auto${NORMAL} value '$auto'."
                    return 1
                    ;;
            esac
            constraint=$(echo "$relation_def" | shyaml get-value constraint auto-pair 2>/dev/null)
            case "$constraint" in
                "required")
                    required+=("$service" "$relation_name" "$relation_def")
                    ;;
                "recommended")
                    recommended+=("$service" "$relation_name" "$relation_def")
                    ;;
                "optional")
                    optional+=("$service" "$relation_name" "$relation_def")
                    ;;
                *)
                    err "Invalid ${WHITE}constraint${NORMAL} value '$contraint'."
                    return 1
                    ;;
            esac
            new_services_uses+=("$service" "$relation_name" "$relation_def")  ## re-queue it
        done
        services_uses=("${new_services_uses[@]}")

        if [ "$changed" ]; then
            continue
        fi
        ## situation is stable

        if [ "${#summon[@]}" != 0 ]; then
            while [ "${#summon[@]}" != 0 ]; do
                service="${summon[0]}"
                relation_name="${summon[1]}"
                relation_def="${summon[2]}"
                summon=("${summon[@]:3}")
                providers=()
                providers_def=()
                array_read-0 providers providers_def < <(_get_charm_providing "$relation_name" "${service_list[@]}")
                if [ "${#providers[@]}" == 0 ]; then
                    die "Summoning a ${DARKBLUE}$relation_name${NORMAL} provider failed: none were found in charm store."
                fi

                if [ "${#providers[@]}" -gt 1 ]; then
                    warn "Auto-summon ${DARKYELLOW}$service${NORMAL}" \
                         "--${DARKBLUE}$relation_name${NORMAL}--> ($DARKYELLOW""${providers[@]}""$NORMAL)"\
                         "(> 1 provider). Choosing first."
                fi
                ts="${providers[0]}"

                ## YYYvlab: should be seen even in no debug mode no ?
                debug "Auto-summon ${DARKYELLOW}$service${NORMAL}" \
                      "--${DARKBLUE}$relation_name${NORMAL}--> ${DARKYELLOW}$ts${NORMAL}"

                _out_new_relation_from_defs "$service" "$relation_name" "$ts" \
                                            "${providers_def[0]}" "$relation_def" \
                                            >> "${cache_file}.wip"

                ## Adding service
                [ "${services[$ts]}" ] && continue
                array_read-0 new_services_uses < <(_get_services_uses "$ts")
                services[$ts]=1
                changed=1
            done
            continue
        fi
        [ "$NO_CONSTRAINT_CHECK" ] && break
        if [ "${#required[@]}" != 0 ]; then
            echo "$(_display_solves required)" | sed -r "s/^/${RED}||${NORMAL} /g" >&2
            err "Required relations not satisfied"
            return 1
        fi
        if [ "${#recommended[@]}" != 0 ]; then
            ## make recommendation
            echo "$(_display_solves recommended)" | sed -r "s/^/${YELLOW}||${NORMAL} /g" >&2
        fi
        if [ "${#optional[@]}" != 0 ]; then
            ## inform about options
            echo "$(_display_solves optional)"  | sed -r "s/^/${BLUE}||${NORMAL} /g" >&2
        fi
        # if [ "${#required[@]}" != 0 ]; then
        #     err "Required relations not satisfied"
        #     return 1
        # fi
        if [ "${#recommended[@]}" != 0 ]; then
            warn "Recommended relations not satisfied"
        fi
        break
    done
    if [ "$?" != 0 ]; then
        rm -f "${cache_file}"{,.wip,.wip.new}  ## no cache
        return 1
    fi
    export ALL_RELATIONS="$cache_file"
    mv "${cache_file}"{.wip,}
    cat "$cache_file"
}
export -f get_all_relations


_display_solves() {
    local array_name="$1" by_relation msg
    ## inform about options
    msg=""
    declare -A by_relation
    while read-0 service relation_name relation_def; do
        solves=$(printf "%s" "$relation_def" | shyaml -y get-value solves 2>/dev/null);
        auto=$(printf "%s" "$relation_def" | shyaml get-value auto 2>/dev/null);
        if [ -z "$solves" ]; then
            continue
        fi
        by_relation[$relation_name]+=$(printf "\n  %s" "${DARKYELLOW}$service$NORMAL for:")
        if [ "$auto" == "pair" ]; then
            requirement="add provider in cluster to auto-pair"
        else
            requirement="add explicit relation"
        fi
        while read-0 name def; do
            by_relation[$relation_name]+=$(printf "\n    - ${DARKCYAN}%-15s${NORMAL} %s (%s)" "$name" "$def" "$requirement")
        done < <(printf "%s" "$solves" | shyaml key-values-0)
    done < <(array_values_to_stdin "$array_name")

    while read-0 relation_name message; do
        msg+="$(printf "\n${DARKBLUE}%s$NORMAL provider is $array_name by%s" \
                        "$relation_name" "$message" )"
    done < <(array_kv_to_stdin by_relation)

    if [ "$msg" ]; then
        printf "%s\n" "${msg:1}"
    fi
}


get_compose_relation_def() {
    local service="$1" relation="$2" relation_name target_service relation_config tech_dep
    while read-0 relation_name target_service relation_config tech_dep; do
        [ "$relation_name" == "$relation" ] || continue
        printf "%s\0%s\0%s\0" "$target_service" "$relation_config" "$tech_dep"
    done < <(get_compose_relations "$service") || return 1
}
export -f get_compose_relation_def


run_service_relations () {
    local service services loaded subservices subservice

    PROJECT_NAME=$(get_default_project_name) || return 1

    export PROJECT_NAME

    declare -A loaded
    subservices=$(get_ordered_service_dependencies "$@") || return 1
    for service in $subservices; do

        # debug "Upping dep's relations of ${DARKYELLOW}$service${NORMAL}:"
        for subservice in $(get_service_deps "$service") "$service"; do
            [ "${loaded[$subservice]}" ] && continue

            export BASE_SERVICE_NAME=$service
            MASTER_BASE_SERVICE_NAME=$(get_top_master_service_for_service "$subservice") || return 1
            MASTER_BASE_CHARM_NAME=$(get_service_charm "$MASTER_BASE_SERVICE_NAME") || return 1
            RELATION_BASE_COMPOSE_DEF=$(get_compose_service_def "$subservice") || return 1
            export RELATION_BASE_COMPOSE_DEF MASTER_BASE_{CHARM,SERVICE}_NAME

            # debug "  Relations of ${DARKYELLOW}$subservice${NORMAL}:"
            while read-0 relation_name target_service relation_config tech_dep; do
                export relation_config

                export TARGET_SERVICE_NAME=$target_service
                MASTER_TARGET_SERVICE_NAME=$(get_top_master_service_for_service "$target_service") || return 1
                MASTER_TARGET_CHARM_NAME=$(get_service_charm "$MASTER_TARGET_SERVICE_NAME") || return 1
                RELATION_TARGET_COMPOSE_DEF=$(get_compose_service_def "$target_service") || return 1
                export RELATION_TARGET_COMPOSE_DEF MASTER_TARGET_{CHARM,SERVICE}_NAME

                Wrap -d "Building $DARKYELLOW$subservice$NORMAL --$DARKBLUE$relation_name$NORMAL--> $DARKYELLOW$target_service$NORMAL" <<EOF || return 1
_run_service_relation "$relation_name" "$subservice" "$target_service" "\$relation_config"
EOF
            done < <(get_service_relations "$subservice") || return 1
            loaded[$subservice]=1
        done
    done
}
export -f run_service_relations


_run_service_action_direct() {
    local service="$1" action="$2" charm _dummy
    shift; shift

    read-0 charm || true  ## against 'set -e' that could be setup in parent scripts

    if read-0 _dummy || [ "$_dummy" ]; then
        print_syntax_error "$FUNCNAME: too many arguments in action descriptor"
        return 1
    fi

    export action_errlvl_file="$state_tmpdir/action-$service-$charm-$action-errlvl"
    export state_tmpdir
    {
        (
            set +e  ## Prevents unwanted leaks from parent shell
            export COMPOSE_CONFIG=$(get_compose_yml_content)
            export METADATA_CONFIG=$(charm.metadata "$charm")
            export SERVICE_NAME=$service
            export ACTION_NAME=$action
            export CONTAINER_NAME=$(get_top_master_service_for_service "$service")
            export DOCKER_BASE_IMAGE=$(service_base_docker_image "$CONTAINER_NAME")
            export SERVICE_DATASTORE="$DATASTORE/$service"
            export SERVICE_CONFIGSTORE="$CONFIGSTORE/$service"
            exname="$exname $ACTION_NAME $SERVICE_NAME" \
                    stdbuf -oL -eL bash -c 'charm.run_direct_action "$@"' -- "$charm" "$action" "$@"
            echo "$?" > "$action_errlvl_file"
        ) | logstdout "$DARKYELLOW$service$NORMAL/${DARKCYAN}$action${NORMAL} ${GREEN}@${NORMAL}"
    } 3>&1 1>&2 2>&3 | logstderr "$DARKYELLOW$service$NORMAL/${DARKCYAN}$action${NORMAL} ${RED}@$NORMAL" 3>&1 1>&2 2>&3
    if ! [ -e "$action_errlvl_file" ]; then
        err "Action $DARKYELLOW$service$NORMAL:$DARKCYAN$action$NORMAL has failed without having time" \
            "to output an errlvl"
        return 1
    fi
    return "$(cat "$action_errlvl_file")"
}
export -f _run_service_action_direct

_run_service_action_relation() {
    local service="$1" action="$2" charm target_charm relation_name relation_config _dummy
    shift; shift

    read-0 charm target_service target_charm relation_name relation_config || true

    if read-0 _dummy || [ "$_dummy" ]; then
        print_syntax_error "$FUNCNAME: too many arguments in action descriptor"
        return 1
    fi

    export RELATION_DATA_FILE=$(get_relation_data_file "$service" "$target_service" "$relation_name" "$relation_config")

    export action_errlvl_file="$state_tmpdir/action-$service-$charm-$action-errlvl"
    export state_tmpdir
    {
        (
            set +e  ## Prevents unwanted leaks from parent shell
            export METADATA_CONFIG=$(charm.metadata "$charm")
            export SERVICE_NAME=$service
            export RELATION_TARGET_SERVICE="$target_service"
            export RELATION_TARGET_CHARM="$target_charm"
            export RELATION_BASE_SERVICE="$service"
            export RELATION_BASE_CHARM="$charm"
            export ACTION_NAME=$action
            export CONTAINER_NAME=$(get_top_master_service_for_service "$service")
            export DOCKER_BASE_IMAGE=$(service_base_docker_image "$CONTAINER_NAME")
            export SERVICE_DATASTORE="$DATASTORE/$service"
            export SERVICE_CONFIGSTORE="$CONFIGSTORE/$service"
            exname="$exname $ACTION_NAME $SERVICE_NAME" \
                    stdbuf -oL -eL bash -c 'charm.run_relation_action "$@"' -- "$target_charm" "$relation_name" "$action" "$@"
            echo "$?" > "$action_errlvl_file"
        ) | logstdout "$DARKYELLOW$service$NORMAL/${DARKCYAN}$action${NORMAL} ${GREEN}@${NORMAL}"
    } 3>&1 1>&2 2>&3 | logstderr "$DARKYELLOW$service$NORMAL/${DARKCYAN}$action${NORMAL} ${RED}@$NORMAL" 3>&1 1>&2 2>&3
    if ! [ -e "$action_errlvl_file" ]; then
        err "Action $DARKYELLOW$service$NORMAL:$DARKCYAN$action$NORMAL has failed without having time" \
            "to output an errlvl"
        return 1
    fi
    return "$(cat "$action_errlvl_file")"
}
export -f _run_service_action_relation


get_relation_data_dir() {
    local service="$1" target_service="$2" relation_name="$3" \
          cache_file="$state_tmpdir/$FUNCNAME.cache.$(printf "%s\0" "$@" | md5_compat)"
    if [ -e "$cache_file" ]; then
        # debug "$FUNCNAME: cache hit ($*)"
        cat "$cache_file"
        return 0
    fi

    project=$(get_default_project_name) || return 1
    relation_dir="$VARDIR/relations/$project/${service}-${target_service}/$relation_name"
    if ! [ -d "$relation_dir" ]; then
        mkdir -p "$relation_dir" || return 1
        chmod go-rwx "$relation_dir" || return 1  ## protecting this directory
    fi
    echo "$relation_dir" | tee "$cache_file"
}
export -f get_relation_data_dir


get_relation_data_file() {
    local service="$1" target_service="$2" relation_name="$3" relation_config="$4"

    relation_dir=$(get_relation_data_dir "$service" "$target_service" "$relation_name") || return 1
    relation_data_file="$relation_dir/data"

    new=
    if [ -e "$relation_data_file" ]; then
        ## Has reference changed ?
        new_md5=$(echo "$relation_config" | md5_compat)
        if [ "$new_md5" != "$(cat "$relation_data_file.md5_ref" 2>/dev/null)" ]; then
            new=true
        fi
    else
        new=true
    fi

    if [ "$new" ]; then
        echo "$relation_config" > "$relation_data_file"
        chmod go-rwx "$relation_data_file"  ## protecting this file
        echo "$relation_config" | md5_compat > "$relation_data_file.md5_ref"
    fi
    echo "$relation_data_file"
}
export -f get_relation_data_file


has_service_action () {
    local service="$1" action="$2" cache_file="$state_tmpdir/$FUNCNAME.cache.$1" \
          charm target_charm relation_name target_service relation_config _tech_dep
    if [ -e "$cache_file" ]; then
        # debug "$FUNCNAME: cache hit ($*)"
        cat "$cache_file"
        return 0
    fi

    charm=$(get_service_charm "$service") || return 1

    ## Action directly provided ?

    if charm.has_direct_action "$charm" "$action" >/dev/null; then
        echo -en "direct\0$charm" | tee "$cache_file"
        return 0
    fi

    ## Action provided by relation ?

    while read-0 relation_name target_service relation_config _tech_dep; do
        target_charm=$(get_service_charm "$target_service") || return 1
        if charm.has_relation_action "$target_charm" "$relation_name" "$action" >/dev/null; then
            echo -en "relation\0$charm\0$target_service\0$target_charm\0$relation_name\0$relation_config" | tee "$cache_file"
            return 0
        fi
    done < <(get_service_relations "$service")

    return 1
    # master=$(get_top_master_service_for_service "$service")
    # [ "$master" == "$charm" ] && return 1

    # has_service_action "$master" "$action"
}
export -f has_service_action


run_service_action () {
    local service="$1" action="$2"
    shift ; shift
    {
        if ! read-0 action_type; then
            info "Service $DARKYELLOW$service$NORMAL does not have any action $DARKCYAN$action$NORMAL defined."
            info "  Add an executable script to 'actions/$action' to implement action."
            return 1
        fi

        Section "running $DARKYELLOW$service$NORMAL/$DARKCYAN$action$NORMAL ($action_type)"; Feed
        "_run_service_action_${action_type}" "$service" "$action" "$@"

    } < <(has_service_action "$service" "$action")
}
export -f run_service_action


get_compose_relation_config() {
    local service=$1 relation_config cache_file="$state_tmpdir/$FUNCNAME.cache.$1"
    if [ -e "$cache_file" ]; then
        # debug "$FUNCNAME: cache hit ($*)"
        cat "$cache_file"
        return 0
    fi

    compose_service_def=$(get_compose_service_def "$service") || return 1

    echo "$compose_service_def" | shyaml get-value "relations" 2>/dev/null | tee "$cache_file"
}
export -f get_compose_relation_config


# ## Return key-values-0
# get_compose_relation_config_for_service() {
#     local service=$1 relation_name=$2 relation_config
#     compose_service_relations=$(get_compose_relation_config "$service") || return 1
#     if ! relation_config=$(
#             echo "$compose_service_relations" |
#                 shyaml get-value "${relation_name}" 2>/dev/null); then
#         err "Couldn't find $DARKYELLOW${service}$NORMAL/${WHITE}${relation_name}$NORMAL" \
#             "relation config in compose configuration."
#         return 1
#     fi
#     if [ -z "$relation_config" ]; then
#         err "Relation ${WHITE}mysql-database$NORMAL is empty in compose configuration."
#         return 1
#     fi
#     if ! echo "$relation_config" | shyaml key-values-0 2>/dev/null; then
#         err "No key/values in ${DARKBLUE}mysql-database$NORMAL of compose config."
#         return 1
#     fi
# }
# export -f get_compose_relation_config_for_service

_get_container_relation() {
    local metadata=$1 found relation_name relation_def

    found=
    while read-0 relation_name relation_def; do
        [ "$(echo "$relation_def" | shyaml get-value "scope" 2>/dev/null)" == "container" ] && {
            found="$relation_name"
            break
        }
    done < <(_get_charm_metadata_uses "$metadata")
    if [ -z "$found" ]; then
        die "Charm $DARKPINK$charm$NORMAL is a subordinate but does not have any required relation declaration with" \
            "${WHITE}scope${NORMAL} set to 'container'."
    fi
    printf "%s" "$found"
}


_get_master_service_for_service_cached () {
    local service="$1" charm="$2" metadata="$3" cache_file="$CACHEDIR/$FUNCNAME.cache.$(echo "$*" | md5_compat)" \
          charm requires master_charm target_charm target_service service_def found
    if [ -e "$cache_file" ]; then
        # debug "$FUNCNAME: STATIC cache hit ($1)"
        cat "$cache_file" &&
        touch "$cache_file" || return 1
        return 0
    fi

    if [ "$(echo "$metadata" | shyaml get-value "subordinate" 2>/dev/null)" != "True" ]; then
        ## just return service name
        echo "$service" | tee "$cache_file"
        return 0
    fi

    ## Action provided by relation ?
    container_relation=$(_get_container_relation "$metadata")
    read-0 target_service _ _ < <(get_service_relation "$service" "$container_relation")
    if [ -z "$target_service" ]; then
        err "Couldn't find ${WHITE}relations.${container_relation}${NORMAL} in" \
            "${DARKYELLOW}$service$NORMAL compose definition."
        err ${FUNCNAME[@]}
        return 1
    fi
    echo "$target_service" | tee "$cache_file"
}
export -f _get_master_service_for_service_cached


get_master_service_for_service() {
    local service="$1" cache_file="$state_tmpdir/$FUNCNAME.cache.$1" \
          charm metadata result

    if [ -e "$cache_file" ]; then
        # debug "$FUNCNAME: SESSION cache hit ($*)"
        cat "$cache_file" || return 1
        return 0
    fi
    charm=$(get_service_charm "$service") || return 1
    metadata=$(charm.metadata "$charm" 2>/dev/null) || {
        metadata=""
        warn "No charm $DARKPINK$charm$NORMAL found."
    }
    result=$(_get_master_service_for_service_cached "$service" "$charm" "$metadata") || return 1
    echo "$result" | tee "$cache_file" || return 1
}
export -f get_master_service_for_service


get_top_master_service_for_service() {
    local service="$1" cache_file="$state_tmpdir/$FUNCNAME.cache.$1" \
          current_service

    if [ -e "$cache_file" ]; then
        # debug "$FUNCNAME: cache hit ($*)"
        cat "$cache_file"
        return 0
    fi

    current_service="$service"
    while true; do
        master_service=$(get_master_service_for_service "$current_service") || return 1
        [ "$master_service" == "$current_service" ] && break
        current_service="$master_service"
    done
    echo "$current_service" | tee "$cache_file"
    return 0
}
export -f get_top_master_service_for_service


##
## The result is a mixin that is not always a complete valid
## docker-compose entry (thinking of subordinates). The result
## will be merge with master charms.
_get_docker_compose_mixin_from_metadata_cached() {
    local service="$1" charm="$2" metadata="$3" \
          has_build_dir="$4" \
          cache_file="$CACHEDIR/$FUNCNAME.cache.$(echo "$*" | md5_compat)" \
          metadata_file metadata volumes docker_compose subordinate image mixin mixins

    if [ -e "$cache_file" ]; then
        #debug "$FUNCNAME: STATIC cache hit $1"
        cat "$cache_file" &&
        touch "$cache_file" || return 1
        return 0
    fi

    mixins=("$(echo -en "labels:\n- compose.charm=$charm")")
    if [ "$metadata" ]; then
        ## resources to volumes
        volumes=$(
            for resource_type in data config; do
                while read-0 resource; do
                    eval "echo \"  - \$${resource_type^^}STORE/\$service\$resource:\$resource:rw\""
                done < <(echo "$metadata" | shyaml get-values-0 "${resource_type}-resources" 2>/dev/null)
            done
            while read-0 resource; do
                if [[ "$resource" ==  /*:/*:* ]]; then
                    echo "  - $resource"
                elif [[ "$resource" ==  /*:/* ]]; then
                    echo "  - $resource:rw"
                elif [[ "$resource" ==  /*:* ]]; then
                    echo "  - ${resource%%:*}:$resource"
                elif [[ "$resource" =~ ^/[^:]+$ ]]; then
                    echo "  - $resource:$resource:rw"
                else
                    die "Invalid host-resource specified in 'metadata.yml'."
                fi
            done < <(printf "%s" "$metadata" | shyaml get-values-0 "host-resources" 2>/dev/null)
            while read-0 resource; do
                dest="$(charm.get_dir "$charm")/resources$resource"
                if ! [ -e "$dest" ]; then
                    die "charm-resource: '$resource' does not exist (file: '$dest')."
                fi
                echo "  - $dest:$resource:ro"
            done < <(echo "$metadata" | shyaml get-values-0 "charm-resources" 2>/dev/null)
               ) || return 1
        if [ "$volumes" ]; then
            mixins+=("volumes:"$'\n'"$volumes")
        fi

        type="$(printf "%s" "$metadata" | shyaml get-value type 2>/dev/null)" || true
        if [ "$type" != "run-once" ]; then
            mixins+=("restart: unless-stopped")
        fi

        docker_compose=$(printf "%s" "$metadata" | shyaml get-value -y "docker-compose" 2>/dev/null) || true
        if [ "$docker_compose" ]; then
            mixins+=("$docker_compose")
        fi

        if [ "$(echo "$metadata" | shyaml get-value "subordinate" 2>/dev/null)" == "True" ]; then
            subordinate=true
        fi
    fi

    image=$(echo "$metadata" | shyaml get-value "docker-image" 2>/dev/null)
    [ "$image" == "None" ] && image=""
    if [ "$image" ]; then
        if [ "$subordinate" ]; then
            err "Subordinate charm can not have a ${WHITE}docker-image${NORMAL} value."
            return 1
        fi
        mixins+=("image: $image")
    elif [ "$has_build_dir" ]; then
        if [ "$subordinate" ]; then
            err "Subordinate charm can not have a 'build' sub directory."
            return 1
        fi
        mixins+=("build: $(charm.get_dir "$charm")/build")
    fi
    mixin=$(merge_yaml_str "${mixins[@]}") || {
        err "Failed to merge mixins from ${DARKPINK}${charm}${NORMAL} metadata."
        return 1
    }
    echo "$mixin" | tee "$cache_file"
}
export -f _get_docker_compose_mixin_from_metadata_cached


get_docker_compose_mixin_from_metadata() {
    local service="$1" cache_file="$state_tmpdir/$FUNCNAME.cache.$1"
    if [ -e "$cache_file" ]; then
        #debug "$FUNCNAME: SESSION cache hit ($*)"
        cat "$cache_file"
        return 0
    fi
    charm=$(get_service_charm "$service") || return 1
    metadata="$(charm.metadata "$charm" 2>/dev/null)" || return 1
    has_build_dir=
    [ -d "$(charm.get_dir "$charm")/build" ] && has_build_dir=true
    mixin=$(_get_docker_compose_mixin_from_metadata_cached "$service" "$charm" "$metadata" "$has_build_dir") || return 1
    echo "$mixin" | tee "$cache_file"
}
export -f get_docker_compose_mixin_from_metadata


_save() {
    local name="$1"
    cat - | tee -a "$docker_compose_dir/.data/$name"
}
export -f _save


get_default_project_name() {
    if [ "$DEFAULT_PROJECT_NAME" ]; then
        echo "$DEFAULT_PROJECT_NAME"
        return 0
    fi
    compose_yml_location="$(get_compose_yml_location)" || return 1
    if [ "$compose_yml_location" ]; then
        if normalized_path=$(readlink -f "$compose_yml_location"); then
            name="$(basename "$(dirname "$normalized_path")")"
            echo "${name%%-deploy}"
            return 0
        fi
    fi
    echo "orphan"
    return 0
}
export -f get_default_project_name


get_running_compose_containers() {
    ## XXXvlab: docker bug: there will be a final newline anyway
    docker ps --filter label="compose.service" --format='{{.ID}}'
}
export -f get_running_compose_containers


get_volumes_for_container() {
    local container="$1"

    docker inspect \
           --format '{{range $mount := .Mounts}}{{$mount.Source}}{{"\x00"}}{{$mount.Destination}}{{"\x00"}}{{end}}' \
           "$container"
}
export -f get_volumes_for_container

is_volume_used() {
    local volume="$1" container_id src dst

    while read container_id; do
        while read-0 src dst; do
            [[ "$src" == "$volume"/* ]] && return 0
        done < <(get_volumes_for_container "$container_id")
    done < <(get_running_compose_containers)
    return 1
}
export -f is_volume_used


clean_unused_docker_compose() {
    for f in /var/lib/compose/docker-compose/*; do
        [ -e "$f" ] || continue
        is_volume_used "$f" && continue
        debug "Cleaning unused docker-compose ${f##*/}"
        rm -rf "$f" || return 1
    done
}
export -f clean_unused_docker_compose


stdin_get_hash() {
    local sha
    sha=$(sha256sum) || return 1
    sha=${sha:0:64}
    echo "$sha"
}
export -f stdin_get_hash


file_get_hash() {
    stdin_get_hash < "$1" || return 1
}
export -f file_get_hash


docker_compose_store() {
    local file="$1" sha

    sha=$(file_get_hash "$file") || return 1
    project=$(get_default_project_name) || return 1
    dst="/var/lib/compose/docker-compose/$sha/$project"
    mkdir -p "$dst" || return 1
    cat <<EOF > "$dst/.env" || return 1
DOCKER_COMPOSE_PATH=$dst
EOF
    cp "$file" "$dst/docker-compose.yml" || return 1

    mkdir -p "$dst/bin" || return 1

    cat <<EOF > "$dst/bin/dc" || return 1
#!/bin/bash

$(declare -f read-0)

docker_run_opts=()

while read-0 opt; do
    docker_run_opts+=("\$opt")
done < <(cat "$COMPOSE_LAUNCHER_OPTS")

docker_run_opts+=(
  "-w" "$dst"
  "--entrypoint" "/usr/local/bin/docker-compose"
)
[ -t 1 ] && {
    docker_run_opts+=("-ti")
}

exec docker run --rm "\${docker_run_opts[@]}" "${COMPOSE_DOCKER_IMAGE:-docker.0k.io/compose}" "\$@"

EOF

    chmod +x "$dst/bin/dc" || return 1

    printf "%s" "$sha"
}


launch_docker_compose() {
    local charm docker_compose_tmpdir docker_compose_dir
    docker_compose_tmpdir=$(mktemp -d -t tmp.XXXXXXXXXX)
    #debug "Creating temporary docker-compose directory in '$docker_compose_tmpdir'."
    trap_add EXIT "rm -rf \"$docker_compose_tmpdir\""

    ## docker-compose will name network from the parent dir name
    project=$(get_default_project_name)
    mkdir -p "$docker_compose_tmpdir/$project"
    docker_compose_dir="$docker_compose_tmpdir/$project"

    get_docker_compose $SERVICE_PACK > "$docker_compose_dir/docker-compose.yml" || return 1
    if [ -e "$state_tmpdir/to-merge-in-docker-compose.yml" ]; then
        # debug "Merging some config data in docker-compose.yml:"
        # debug "$(cat $state_tmpdir/to-merge-in-docker-compose.yml)"
        _config_merge "$docker_compose_dir/docker-compose.yml" "$state_tmpdir/to-merge-in-docker-compose.yml" || return 1
    fi

    if [ -z "$(echo $(cat "$docker_compose_dir/docker-compose.yml"))" ]; then
        die "Generated 'docker-compose.yml' is unexpectedly empty."
    fi

    ## XXXvlab: could be more specific and only link the needed charms
    ## XXXvlab: why do we need these links ? If this is for the build command, then it is not useful anymore.
    # for charm in $(shyaml keys services < "$docker_compose_dir/docker-compose.yml"); do
    #     if charm.exists "$charm"; then
    #         ln -sf "$(charm.get_dir "$charm")" "$docker_compose_dir/$charm" || exit 1
    #     fi
    # done
    mkdir "$docker_compose_dir/.data"

    if [ -z "$COMPOSE_DISABLE_DOCKER_COMPOSE_STORE" ]; then
        sha=$(docker_compose_store "$docker_compose_dir/docker-compose.yml") || return 1
    fi

    {
        {
            if [ -z "$COMPOSE_DISABLE_DOCKER_COMPOSE_STORE" ]; then
                cd "/var/lib/compose/docker-compose/$sha/$project"
            else
                cd "$docker_compose_dir"
            fi
            if [ -f ".env" ]; then
                debug "${WHITE}.env$NORMAL for $DARKYELLOW$SERVICE_PACK$NORMAL:"
                debug "$(cat ".env" | prefix "  $GRAY|$NORMAL ")"
            fi
            debug "${WHITE}docker-compose.yml$NORMAL for $DARKYELLOW$SERVICE_PACK$NORMAL:"
            debug "$(cat "docker-compose.yml" | prefix "  $GRAY|$NORMAL ")"
            debug "${WHITE}Launching$NORMAL: docker-compose $@"
            if [ "$DRY_COMPOSE_RUN" ]; then
                echo docker-compose "$@"
            else
                docker-compose "$@"
            fi
            echo "$?" > "$docker_compose_dir/.data/errlvl"
        } | _save stdout
    } 3>&1 1>&2 2>&3 | _save stderr 3>&1 1>&2 2>&3
    if tail -n 1 "$docker_compose_dir/.data/stderr" | egrep "Service .+ failed to build: Error getting container [0-9a-f]+ from driver devicemapper: (open|Error mounting) /dev/mapper/docker-.*: no such file or directory$" >/dev/null 2>&1; then
        err "Detected bug https://github.com/docker/docker/issues/4036 ... "
        err "Please re-launch your command, or switch from 'devicemapper' driver to 'overlayfs' or 'aufs'."
    fi

    docker_compose_errlvl="$(cat "$docker_compose_dir/.data/errlvl" 2>/dev/null)"
    if [ -z "$docker_compose_errlvl" ]; then
        err "Something went wrong before you could gather docker-compose errorlevel."
        return 1
    fi
    return "$docker_compose_errlvl"
}
export -f launch_docker_compose


get_compose_yml_location() {
    if ! [ -z ${COMPOSE_YML_FILE+x} ]; then   ## if set, even if empty
        echo "$COMPOSE_YML_FILE"
        return 0
    fi
    parent=$(while ! [ -f "./compose.yml" ]; do
                 [ "$PWD" == "/" ] && exit 0
                 cd ..
             done; echo "$PWD"
          )
    if [ "$parent" ]; then
        echo "$parent/compose.yml"
        return 0
    fi
    ## XXXvlab: do we need this additional environment variable,
    ## COMPOSE_YML_FILE is not sufficient ?
    if [ "$DEFAULT_COMPOSE_FILE" ]; then
        if ! [ -e "$DEFAULT_COMPOSE_FILE" ]; then
            warn "No 'compose.yml' was found in current or parent dirs," \
                "and \$DEFAULT_COMPOSE_FILE points to an unexistent file." \
                "(${DEFAULT_COMPOSE_FILE})"
            return 0
        fi
        echo "$DEFAULT_COMPOSE_FILE"
        return 0
    fi
    warn "No 'compose.yml' was found in current or parent dirs, and no \$DEFAULT_COMPOSE_FILE was set."
    return 0
}
export -f get_compose_yml_location


get_compose_yml_content() {
    local cache_file="$state_tmpdir/$FUNCNAME.cache"
    if [ -e "$cache_file" ]; then
        cat "$cache_file" &&
        touch "$cache_file" || return 1
        return 0
    fi

    if [ -z "$COMPOSE_YML_FILE" ]; then
        COMPOSE_YML_FILE=$(get_compose_yml_location) || exit 1
    fi
    if [ -e "$COMPOSE_YML_FILE" ]; then
        debug "Found $WHITE$exname$NORMAL YAML file in '$COMPOSE_YML_FILE'."
        COMPOSE_YML_CONTENT=$(cat "$COMPOSE_YML_FILE") || {
            err "Could not read '$COMPOSE_YML_FILE'."
            return 1
        }
    else
        debug "No compose file found. Using an empty one."
        COMPOSE_YML_CONTENT=""
    fi

    COMPOSE_YML_CONTENT=$(merge_yaml_str "$COMPOSE_YML_CONTENT" "${compose_contents[@]}") || return 1
    output=$(echo "$COMPOSE_YML_CONTENT"| shyaml get-value 2>&1)
    if [ "$?" != 0 ]; then
        outputed_something=
        while IFS='' read -r line1 && IFS='' read -r line2; do
            [ "$outputed_something" ] || err "Invalid YAML in '$COMPOSE_YML_FILE':"
            outputed_something=true
            echo "$line1 $GRAY($line2)$NORMAL"
        done < <(echo "$output" | grep ^yaml.scanner -A 100 |
                     sed -r 's/^  in "<stdin>", //g' | sed -r 's/^yaml.scanner.[a-zA-Z]+: //g') |
            prefix "  $GRAY|$NORMAL "
        [ "$outputed_something" ] || {
            err "Unexpected error while running 'shyaml get-value' on '$COMPOSE_YML_FILE':"
            echo "$output" | prefix "  $GRAY|$NORMAL "
        }
        return 1
    fi
    echo "$COMPOSE_YML_CONTENT" | tee "$cache_file" || return 1
}
export -f get_compose_yml_content


get_default_target_services() {
    local services=("$@")
    if [ -z "${services[*]}"  ]; then
        if [ "$DEFAULT_SERVICES" ]; then
            debug "No service provided, using $WHITE\$DEFAULT_SERVICES$NORMAL variable." \
                 "Target services: $DARKYELLOW$DEFAULT_SERVICES$NORMAL"
            services="$DEFAULT_SERVICES"
        else
            err "No service provided."
            return 1
        fi
    fi
    echo "${services[*]}"
}
export -f get_default_target_services


get_master_services() {
    local loaded master_service service
    declare -A loaded
    for service in "$@"; do
        master_service=$(get_top_master_service_for_service "$service") || return 1
        if [ "${loaded[$master_service]}" ]; then
            continue
        fi
        echo "$master_service"
        loaded["$master_service"]=1
    done | xargs printf "%s "
    return "${PIPESTATUS[0]}"
}
export -f get_master_services


get_current_docker_container_id() {
    local line
    line=$(cat "/proc/self/cpuset") || return 1
    [[ "$line" == *docker* ]] || return 1
    echo "${line##*/}"
}
export -f get_current_docker_container_id


## if we are in a docker compose, we might want to know what is the
## real host path of some local paths.
get_host_path() {
    local path="$1"
    path=$(realpath "$path") || return 1
    container_id=$(get_current_docker_container_id) || {
        print "%s" "$path"
        return 0
    }
    biggest_dst=
    current_src=
    while read-0 src dst; do
        [[ "$path" == "$dst"* ]] || continue
        if [[ "${#biggest_dst}" < "${#dst}" ]]; then
            biggest_dst="$dst"
            current_src="$src"
        fi
    done < <(get_volumes_for_container  "$container_id")
    if [ "$current_src" ]; then
        printf "%s" "$current_src"
    else
        return 1
    fi
}
export -f get_host_path


_setup_state_dir() {
    export state_tmpdir=$(mktemp -d -t tmp.XXXXXXXXXX)
    #debug "Creating temporary state directory in '$state_tmpdir'."
#     trap_add EXIT "debug \"Removing temporary state directory in $state_tmpdir.\";\
# rm -rf \"$state_tmpdir\""
    trap_add EXIT "rm -rf \"$state_tmpdir\""
}


get_docker_compose_help_msg() {
    local action="$1" cache_file="$CACHEDIR/$FUNCNAME.cache.$(echo "$1"; cat "$(which docker-compose)" | md5_compat)" \
          docker_compose_help_msg
    if [ -e "$cache_file" ]; then
        cat "$cache_file" &&
        touch "$cache_file" || return 1
        return 0
    fi
    docker_compose_help_msg=$(docker-compose $action --help 2>/dev/null) || return 1
    echo "$docker_compose_help_msg" |
        tee "$cache_file" || return 1
}


get_docker_compose_usage() {
    local action="$1" cache_file="$CACHEDIR/$FUNCNAME.cache.$(echo "$1"; cat "$(which docker-compose)" | md5_compat)" \
          docker_compose_help_msg
    if [ -e "$cache_file" ]; then
        cat "$cache_file" &&
        touch "$cache_file" || return 1
        return 0
    fi
    docker_compose_help_msg=$(get_docker_compose_help_msg $action) || return 1
    echo "$docker_compose_help_msg" |
        grep -m 1 "^Usage:" -A 10000 |
        egrep -m 1 "^\$"    -B 10000 |
        xargs printf "%s " |
        sed -r 's/^Usage: //g' |
        tee "$cache_file" || return 1
}


get_docker_compose_opts_help() {
    local action="$1" cache_file="$CACHEDIR/$FUNCNAME.cache.$(echo "$1"; cat "$(which docker-compose)" | md5_compat)" \
          docker_compose_help_msg
    if [ -e "$cache_file" ]; then
        cat "$cache_file" &&
        touch "$cache_file" || return 1
        return 0
    fi
    docker_compose_opts_help=$(get_docker_compose_help_msg $action) || return 1
    echo "$docker_compose_opts_help" |
        grep '^Options:' -A 20000 |
        tail -n +2 |
        { cat ; echo; } |
        egrep -m 1 "^\S*\$" -B 10000 |
        head -n -1 |
        tee "$cache_file" || return 1
}


get_docker_compose_commands_help() {
    local action="$1" cache_file="$CACHEDIR/$FUNCNAME.cache.$(echo "$1"; cat "$(which docker-compose)" | md5_compat)" \
          docker_compose_help_msg
    if [ -e "$cache_file" ]; then
        cat "$cache_file" &&
        touch "$cache_file" || return 1
        return 0
    fi
    docker_compose_opts_help=$(get_docker_compose_help_msg $action) || return 1
    echo "$docker_compose_opts_help" |
        grep '^Commands:' -A 20000 |
        tail -n +2 |
        { cat ; echo; } |
        egrep -m 1 "^\S*\$" -B 10000 |
        head -n -1 |
        tee "$cache_file" || return 1
}


get_docker_compose_opts_list() {
    local action="$1" cache_file="$CACHEDIR/$FUNCNAME.cache.$(echo "$1"; cat "$(which docker-compose)" | md5_compat)" \
          docker_compose_help_msg
    if [ -e "$cache_file" ]; then
        cat "$cache_file" &&
        touch "$cache_file" || return 1
        return 0
    fi
    docker_compose_opts_help=$(get_docker_compose_opts_help $action) || return 1
    echo "$docker_compose_opts_help" |
        egrep "^\s+-" |
        sed -r 's/\s+((((-[a-zA-Z]|--[a-zA-Z0-9-]+)( [A-Z=]+|=[^ ]+)?)(, )?)+)\s+.*$/\1/g' |
        tee "$cache_file" || return 1
}


options_parser() {
    sed -r 's/^(\s+(((-[a-zA-Z]|--[a-zA-Z0-9-]+)([ =]([a-zA-Z_=\"\[]|\])+)?(, | )?)+)\s+)[^ ].*$/\x0\2\x0\0/g'
    printf "\0"
}


remove_options_in_option_help_msg() {
   {
        read-0 null
        if [ "$null" ]; then
            err "options parsing error, should start with an option line."
            return 1
        fi
        while read-0 opt full_txt;do
            multi_opts="$(printf "%s " $opt | multi_opts_filter)"
            single_opts="$(printf "%s " $opt | single_opts_filter)"
            for to_remove in "$@"; do
                str_matches "$to_remove" $multi_opts $single_opts && {
                    continue 2
                }
            done
            echo -n "$full_txt"
        done
    } < <(options_parser)
}


_MULTIOPTION_REGEX='^((-[a-zA-Z]|--[a-zA-Z0-9-]+)(, )?)+'
_MULTIOPTION_REGEX_LINE_FILTER=$_MULTIOPTION_REGEX'(\s|=)'


multi_opts_filter() {
     egrep "$_MULTIOPTION_REGEX_LINE_FILTER" |
        sed -r "s/^($_MULTIOPTION_REGEX)(\s|=).*$/\1/g" |
        tr ',' "\n" | xargs printf "%s "
}


single_opts_filter() {
    egrep -v "$_MULTIOPTION_REGEX_LINE_FILTER" |
        tr ',' "\n" | xargs printf "%s "
}


get_docker_compose_multi_opts_list() {
    local action="$1" opts_list
    opts_list=$(get_docker_compose_opts_list "$action") || return 1
    echo "$opts_list" | multi_opts_filter
}


get_docker_compose_single_opts_list() {
    local action="$1" opts_list
    opts_list=$(get_docker_compose_opts_list "$action") || return 1
    echo "$opts_list" | single_opts_filter
}

display_commands_help() {
    local charm_actions
    echo
    echo "${WHITE}Commands${NORMAL} (thanks to docker-compose):"
    get_docker_compose_commands_help | sed -r "s/  ([a-z]+)(\s+)/  ${DARKCYAN}\1${NORMAL}\2/g"
    charm_actions_help=$(get_docker_charm_action_help) || return 1
    if [ "$charm_actions_help" ]; then
        echo
        echo "${WHITE}Charm actions${NORMAL}:"
        printf "%s\n" "$charm_actions_help" | \
            sed -r "s/^    ([a-z0-9-]+)(\s+)([a-z0-9-]+)(\s+)/    ${DARKCYAN}\1${NORMAL}\2${DARKYELLOW}\3${NORMAL}\4/g"
    fi
}

get_docker_charm_action_help() {
    local services service charm relation_name target_service relation_config \
          target_charm
    services=($(get_compose_yml_content | shyaml keys 2>/dev/null))
    for service in "${services[@]}"; do
        out=$(
            charm=$(get_service_charm "$service") || return 1
            for action in $(charm.ls_direct_actions "$charm"); do
                printf "    %-28s %s\n" "$action $service" "Direct action from ${DARKPINK}$charm${NORMAL}"
            done
            while read-0 relation_name target_service _relation_config _tech_dep; do
                target_charm=$(get_service_charm "$target_service") || return 1
                for action in $(charm.ls_relation_actions "$target_charm" "$relation_name"); do
                    printf "    %-28s %s\n" "$action $service" "Indirect action from ${DARKPINK}$target_charm${NORMAL}"
                done
            done < <(get_compose_relations "$service")
        )
        if [ "$out" ]; then
            echo "  for ${DARKYELLOW}$service${NORMAL}:"
            printf "%s\n" "$out"
        fi
    done
}

display_help() {
    print_help
    echo "${WHITE}Options${NORMAL}:"
    echo "  -h, --help                  Print this message and quit"
    echo "                              (ignoring any other options)"
    echo "  -V, --version               Print current version and quit"
    echo "                              (ignoring any other options)"
    echo "  --dirs                      Display data dirs and quit"
    echo "                              (ignoring any other options)"
    echo "  -v, --verbose               Be more verbose"
    echo "  -d, --debug                 Print full debugging information (sets also verbose)"
    echo "  --dry-compose-run           If docker-compose will be run, only print out what"
    echo "                              command line will be used."
    echo "  --rebuild-relations-to-service, -R SERVICE"
    echo "                              Will rebuild all relations to given service"
    echo "  --add-compose-content, -Y YAML"
    echo "                              Will merge some direct YAML with the current compose"

    get_docker_compose_opts_help | remove_options_in_option_help_msg --version --help --verbose |
        filter_docker_compose_help_message
    display_commands_help
}


_graph_service() {
    local service="$1" base="$1"

    charm=$(get_service_charm "$service") || return 1
    metadata=$(charm.metadata "$charm") || return 1
    subordinate=$(echo "$metadata" | shyaml get-value "subordinate" 2>/dev/null)

    if [ "$subordinate" == "True" ]; then
        requires="$(echo "$metadata" | shyaml get-value "requires" 2>/dev/null)"
        master_charm=
        while read-0 relation_name relation; do
            [ "$(echo "$relation" | shyaml get-value "scope" 2>/dev/null)" == "container" ] || continue
            interface="$(echo "$relation" | shyaml get-value "interface" 2>/dev/null)"
            if [ -z "$interface" ]; then
                err "No ${WHITE}$interface${NORMAL} set for relation $relation_name."
                return 1
            fi

            ## Action provided by relation ?

            target_service=
            while read-0 relation_name candidate_target_service _relation_config _tech_dep; do
                [ "$interface" == "$relation_name" ] && {
                    target_service="$candidate_target_service"
                    break
                }
            done < <(get_service_relations "$service")
            if [ -z "$target_service" ]; then
                err "Couldn't find ${WHITE}relations.$interface${NORMAL} in" \
                    "${DARKYELLOW}$service$NORMAL compose definition."
                return 1
            fi
            master_service="$target_service"
            master_charm=$(get_service_charm "$target_service") || return 1
            break
        done < <(echo "$requires" | shyaml key-values-0 2>/dev/null)
    fi

    _graph_node_service "$service" "$base" "$charm"
    _graph_edge_service "$service" "$subordinate" "$master_service"

}


_graph_node_service() {
    local service="$1" base="$2" charm="$3"

    cat <<EOF
  "$(_graph_node_service_label ${service})" [
     style = "filled, $([ "$subordinate" == "True" ] && echo "dashed" || echo "bold")"
     penwidth = $([ "$subordinate" == "True" ] && echo "3" || echo "5")
     color = $([ "$base" ] && echo "blue" || echo "black")
     fillcolor = "white"
     fontname = "Courier New"
     shape = "Mrecord"
     label =<$(_graph_node_service_content "$service")>
     ];
EOF

}


_graph_edge_service() {
    local service="$1" subordinate="$2" master_service="$3"
    while read-0 relation_name target_service relation_config tech_dep; do
        cat <<EOF
"$(_graph_node_service_label ${service})" -> "$(_graph_node_service_label ${target_service})" [
    penwidth = $([ "$master_service" == "$target_service" ] && echo 3 || echo 2)

    fontsize = 16
    fontcolor = "black"
    style = $([ "$master_service" == "$target_service" ] && echo dashed || echo "\"\"")
    weight = $([ "$master_service" == "$target_service" ] && echo 2.0 || echo 1.0)
    dir = $([ "$master_service" == "$target_service" ] && echo none || echo both)
    arrowtail = odot
    # arrowhead = dotlicurve
    taillabel = "$relation_name" ];
EOF
    done < <(get_service_relations "$service") || return 1
}


_graph_node_service_label() {
    local service="$1"
    echo "service_$service"
}


_graph_node_service_content() {
    local service="$1"
    charm=$(get_service_charm "$service") || return 1

cat <<EOF
<table border="0" cellborder="0" cellpadding="3" bgcolor="white">
  <tr>
    <td bgcolor="black" align="center" colspan="2">
       <font color="white">$service</font>
    </td>
  </tr>
$(if [ "$charm" != "$service" ]; then 
cat <<EOF2
<tr>
    <td align="left" port="r0">charm: $charm</td>
  </tr>
EOF2
fi)
</table>
EOF
}


cla_contains () {
    local e
    for e in "${@:2}"; do [[ "$e" == "$1" ]] && return 0; done
    return 1
}


filter_docker_compose_help_message() {
    cat - |
        sed -r "s/docker-compose run/${DARKWHITE}compose${NORMAL} ${DARKCYAN}$action${NORMAL}/g;
                s/docker-compose.yml/compose.yml/g;
                s/SERVICES?/${DARKYELLOW}\0${NORMAL}/g;
                s/^(\s+)\\$/\1${WHITE}\$${NORMAL}/g;
                s/^(\s+)run/\1${DARKCYAN}$action${NORMAL}/g;
                s/docker-compose/${DARKWHITE}compose${NORMAL}/g"
}


graph() {
    local services=("$@")
    declare -A entries
    cat <<EOF
digraph g {
  graph [
      fontsize=30 
      labelloc="t" 
      label="" 
      splines=true
      overlap=false 
      #rankdir = "LR"
  ];
  ratio = auto;
EOF
    for target_service in "$@"; do
        services=$(get_ordered_service_dependencies "$target_service") || return 1
        for service in $services; do
            [ "${entries[$service]}" ] && continue || entries[$service]=1
            if cla_contains "$service" "${services[@]}"; then
                base=true
            else
                base=
            fi
            _graph_service "$service" "$base"
        done
    done
    echo "}"
}


cached_wget() {
    local cache_file="$CACHEDIR/$FUNCNAME.cache.$(echo "$*" | md5_compat)" \
          url="$1"
    if [ -e "$cache_file" ]; then
        cat "$cache_file"
        touch "$cache_file"
        return 0
    fi
    wget -O- "${url}" |
        tee "$cache_file"

    if [ "${PIPESTATUS[0]}" != 0 ]; then
        rm "$cache_file"
        die "Unable to fetch '$url'."
        return 1
    fi
}
export -f cached_wget


[ "$SOURCED" ] && return 0


trap_add "EXIT" clean_cache



if [ -z "$DISABLE_SYSTEM_CONFIG_FILE" ]; then
    if [ -r /etc/default/charm ]; then
        . /etc/default/charm
    fi

    if [ -r "/etc/default/$exname" ]; then
        . "/etc/default/$exname"
    fi

    ## XXXvlab: should provide YML config opportunities in possible parent dirs ?
    ##  userdir ? and global /etc/compose.yml ?
    for cfgfile in /etc/compose.conf /etc/compose.local.conf \
                   /etc/default/compose /etc/compose/local.conf; do
        [ -e "$cfgfile" ] || continue
        . "$cfgfile" || die "Loading config file '$cfgfile' failed."
    done
fi

_setup_state_dir
mkdir -p "$CACHEDIR" || exit 1

##
## Argument parsing
##

services=()
remainder_args=()
compose_opts=()
compose_contents=()
action_opts=()
services_args=()
pos_arg_ct=0
no_hooks=
no_init=
action=
stage="main"  ## switches from 'main', to 'action', 'remainder'
is_docker_compose_action=
rebuild_relations_to_service=()
DC_MATCH_MULTI=$(get_docker_compose_multi_opts_list) &&
    DC_MATCH_SINGLE=$(get_docker_compose_single_opts_list) || return 1
while read-0 arg; do
    case "$stage" in
        "main")
            case "$arg" in
                --help|-h)
                    no_init=true ; no_hooks=true ; no_relations=true
                    display_help
                    exit 0
                    ;;
                --verbose|-v)
                    export VERBOSE=true
                    compose_opts+=("--verbose")
                    ;;
                --version|-V)
                    print_version
                    docker-compose --version
                    docker --version
                    exit 0
                    ;;
                -f|--file)
                    read-0 value
                    [ -e "$value" ] || die "File $value doesn't exists"
                    export COMPOSE_YML_FILE="$value"
                    shift
                    ;;
                -p|--project-name)
                    read-0 value
                    export DEFAULT_PROJECT_NAME="$value"
                    compose_opts+=("--project-name $value")
                    shift
                    ;;
                --no-relations)
                    export no_relations=true
                    ;;
                --no-hooks)
                    export no_hooks=true
                    ;;
                --no-init)
                    export no_init=true
                    ;;
                --rebuild-relations-to-service|-R)
                    read-0 value
                    rebuild_relations_to_service+=("$value")
                    shift
                    ;;
                --debug)
                    export DEBUG=true
                    export VERBOSE=true
                    #compose_opts+=("--verbose" "--log-level" "DEBUG")
                    ;;
                --add-compose-content|-Y)
                    read-0 value
                    compose_contents+=("$value")
                    shift
                    ;;
                --dirs)
                    echo "CACHEDIR: $CACHEDIR"
                    echo "VARDIR: $VARDIR"
                    exit 0
                    ;;
                --dry-compose-run)
                    export DRY_COMPOSE_RUN=true
                    ;;
                --*|-*)
                    if str_pattern_matches "$arg" $DC_MATCH_MULTI; then
                        read-0 value
                        compose_opts+=("$arg" "$value")
                        shift;
                    elif str_pattern_matches "$arg" $DC_MATCH_SINGLE; then
                        compose_opts+=("$arg")
                    else
                        err "Unknown option '$arg'. Please check help:"
                        display_help
                        exit 1
                    fi
                    ;;
                *)
                    action="$arg"
                    stage="action"
                    if DC_USAGE=$(get_docker_compose_usage "$action"); then
                        is_docker_compose_action=true
                        DC_MATCH_MULTI=$(get_docker_compose_multi_opts_list "$action") &&
                            DC_MATCH_SINGLE="$(get_docker_compose_single_opts_list "$action")"
                        if [ "$DC_MATCH_MULTI" ]; then
                            DC_MATCH_SINGLE="$DC_MATCH_SINGLE $(echo "$DC_MATCH_MULTI" | sed -r 's/( |$)/=\* /g')"
                        fi
                        pos_args=($(echo "$DC_USAGE" | sed -r 's/\[-[^]]+\] ?//g;s/\[options\] ?//g'))
                        pos_args=("${pos_args[@]:1}")
                        # echo "USAGE: $DC_USAGE"
                        # echo "pos_args: ${pos_args[@]}"
                        # echo "MULTI: $DC_MATCH_MULTI"
                        # echo "SINGLE: $DC_MATCH_SINGLE"
                        # exit 1
                    else
                        stage="remainder"
                    fi
                    ;;
            esac
            ;;
        "action")   ## Only for docker-compose actions
            case "$arg" in
                --help|-h)
                    no_init=true ; no_hooks=true ; no_relations=true
                    action_opts+=("$arg")
                    ;;
                --*|-*)
                    if [ "$is_docker_compose_action" ]; then
                        if str_pattern_matches "$arg" $DC_MATCH_MULTI; then
                            read-0 value
                            action_opts+=("$arg" "$value")
                            shift
                        elif str_pattern_matches "$arg" $DC_MATCH_SINGLE; then
                            action_opts+=("$arg")
                        else
                            err "Unknown option '$arg'. Please check '${DARKCYAN}$action${NORMAL}' help:"
                            docker-compose "$action" --help |
                                filter_docker_compose_help_message >&2
                            exit 1
                        fi
                    fi
                    ;;
                *)
                    # echo "LOOP $1 : pos_arg: $pos_arg_ct // ${pos_args[$pos_arg_ct]}"
                    if [[ "${pos_args[$pos_arg_ct]}" == "[SERVICE...]" ]]; then
                        services_args+=("$arg")
                    elif [[ "${pos_args[$pos_arg_ct]}" == "SERVICE" ]]; then
                        services_args=("$arg") || exit 1
                        stage="remainder"
                    else
                        action_posargs+=("$arg")
                        ((pos_arg_ct++))
                    fi
                    ;;
            esac
            ;;
        "remainder")
            remainder_args+=("$arg")
            while read-0 arg; do
                remainder_args+=("$arg")
            done
            break 3
            ;;
    esac
    shift
done < <(cla.normalize "$@")


export compose_contents
[ "${services_args[*]}" ] && debug "  ${DARKWHITE}Services:$NORMAL ${DARKYELLOW}${services_args[*]}$NORMAL"
[ "${compose_opts[*]}" ] && debug "  ${DARKWHITE}Main docker-compose opts:$NORMAL ${compose_opts[*]}"
[ "${action_posargs[*]}" ] && debug "  ${DARKWHITE}Main docker-compose pos args:$NORMAL ${action_posargs[*]}"
[ "${action_opts[*]}" ] && debug "  ${DARKWHITE}Action $DARKCYAN$action$NORMAL with opts:$NORMAL ${action_opts[*]}"
[ "${remainder_args[*]}" ] && debug "  ${DARKWHITE}Remainder args:$NORMAL ${remainder_args[*]}"



aexport remainder_args


##
## Actual code
##


COMPOSE_YML_FILE=$(get_compose_yml_location) || exit 1
COMPOSE_YML_CONTENT=$(get_compose_yml_content) || exit 1
export COMPOSE_YML_FILE COMPOSE_YML_CONTENT


charm.sanity_checks || die "Sanity checks about charm-store failed. Please correct."

##
## Get services in command line.
##

if [ -z "$is_docker_compose_action" -a "$action" ]; then
    action_service=${remainder_args[0]}
    if [ -z "$action_service" ]; then
        err "No such command or action: ${DARKCYAN}$action${NORMAL}"
        display_commands_help
        exit 1
    fi
    remainder_args=("${remainder_args[@]:1}")
    if has_service_action "$action_service" "$action" >/dev/null; then
        is_service_action=true
        {
            read-0 action_type
            case "$action_type" in
                "relation")
                    read-0 _ target_service _target_charm relation_name
                    debug "Found action $DARKYELLOW${action_service}$NORMAL/$DARKBLUE$relation_name$NORMAL/$DARKCYAN$action$NORMAL (in $DARKYELLOW$target_service$NORMAL)"
                    ;;
                "direct")
                    debug "Found action $DARKYELLOW${action_service}$NORMAL.$DARKCYAN$action$NORMAL"
                    ;;
            esac
        } < <(has_service_action "$action_service" "$action")
        services_args=("$action_service")
    else
        die "Unknown action '${DARKCYAN}$action$NORMAL': It doesn't match any docker-compose commands nor inner charm actions."
    fi
else
    case "$action" in
        ps|up)
            if [ "${#services_args[@]}" == 0 ]; then
                array_read-0 services_args < <(printf "%s" "$COMPOSE_YML_CONTENT" | shyaml keys-0 2>/dev/null)
            fi
            ;;
        config)
            services_args=("${action_posargs[@]}")
            ;;
    esac
fi


NO_CONSTRAINT_CHECK=True
case "$action" in
    up)
        NO_CONSTRAINT_CHECK=
        ;;
esac


get_all_relations "${services_args[@]}" >/dev/null || exit 1

if [ "$is_docker_compose_action" -a "${#services_args[@]}" -gt 0 ]; then
    services=($(get_master_services "${services_args[@]}")) || exit 1
    if [ "$action" == "up" ]; then
        ## remove run-once
        for service in "${services_args[@]}"; do
            type="$(get_service_type "$service")" || exit 1
            if [ "$type" != "run-once" ]; then
                action_posargs+=("$service")
            fi
        done
    else
        action_posargs+=("${services[@]}")
    fi
fi


get_docker_compose "${services_args[@]}" >/dev/null || {  ## precalculate variable \$_current_docker_compose
    err "Fails to compile base 'docker-compose.yml'"
    exit 1
}

##
## Pre-action
##


full_init=
case "$action" in
    up|run)
        full_init=true
        post_hook=true
        ;;
    ""|down|restart|logs|config|ps)
        full_init=
        ;;
    *)
        if [ "$is_service_action" ]; then
            full_init=true
        fi
        ;;
esac


if [ "$full_init" ]; then
    ## init in order
    if [ -z "$no_init" ]; then
        Section setup host resources
        setup_host_resources "${services_args[@]}" || exit 1
        Section initialisation
        run_service_hook init "${services_args[@]}" || exit 1
    fi

    ## Get relations
    if [ -z "$no_relations" ]; then
        if [ "${#rebuild_relations_to_service[@]}" != 0 ]; then
            rebuild_relations_to_service=$(get_master_services "${rebuild_relations_to_service[@]}") || return 1
            rebuild_relations_to_service=($rebuild_relations_to_service)
            project=$(get_default_project_name) || return 1
            for service in "${rebuild_relations_to_service[@]}"; do
                for dir in "$VARDIR/relations/$project/"*"-${service}/"*; do
                    [ -d "$dir" ] && {
                        debug rm -rf "$dir"
                        rm -rf "$dir"
                    }
                done
            done
        fi
        run_service_relations "${services_args[@]}" || exit 1
    fi

    run_service_hook pre_deploy "${services_args[@]}" || exit 1

fi

if [ "$action" == "run" -a "${#services_args}" != 0 ]; then
    charm=$(get_service_charm "${services_args[0]}") || exit 1
    metadata=$(charm.metadata "$charm") || exit 1
    type="$(printf "%s" "$metadata" | shyaml get-value type 2>/dev/null)" || true
    if [ "$type" == "run-once" ]; then
        run_service_hook dc-pre-run "${services_args[@]}" || exit 1
    fi
fi


export SERVICE_PACK="${services_args[*]}"

##
## Docker-compose
##

case "$action" in
    up|start|stop|build|run)
        ## force daemon mode for up
        if [[ "$action" == "up" ]]; then
            if ! array_member action_opts -d; then
                action_opts+=("-d")
            fi
            if ! array_member action_opts --remove-orphans; then
                action_opts+=("--remove-orphans")
            fi
        fi
        launch_docker_compose "${compose_opts[@]}" "$action" "${action_opts[@]}" "${action_posargs[@]}" "${remainder_args[@]}"
        ;;
    logs)
        if ! array_member action_opts --tail; then  ## force daemon mode for up
            action_opts+=("--tail" "10")
        fi
        launch_docker_compose "${compose_opts[@]}" "$action" "${action_opts[@]}" "${action_posargs[@]}" "${remainder_args[@]}"
        ;;
    "")
        launch_docker_compose "${compose_opts[@]}"
        ;;
    graph)
        graph $SERVICE_PACK
        ;;
    config)
        ## removing the services
        services=($(get_master_services "${action_posargs[@]}")) || exit 1
        ## forcing docker-compose config to output the config file to stdout and not stderr
        out=$(launch_docker_compose "${compose_opts[@]}" "$action" "${action_opts[@]}" "${remainder_args[@]}" 2>&1) || {
            echo "$out"
            exit 1
        }
        echo "$out"
        warn "Runtime configuration modification (from relations) are not included here."
        ;;
    down)
        if ! array_member action_opts --remove-orphans; then  ## force daemon mode for up
            debug "Adding a default argument of '--remove-orphans'"
            action_opts+=("--remove-orphans")
        fi
        launch_docker_compose "${compose_opts[@]}" "$action" "${action_opts[@]}" "${remainder_args[@]}"
        ;;
    *)
        if [ "$is_service_action" ]; then
            run_service_action "$SERVICE_PACK" "$action" "${remainder_args[@]}"
        else
            launch_docker_compose "${compose_opts[@]}" "$action" "${action_opts[@]}" "${action_posargs[@]}" "${remainder_args[@]}"
        fi
        ;;
esac || exit 1


if [ "$post_hook" -a "${#services_args[@]}" != 0 ]; then
    run_service_hook post_deploy "${services_args[@]}" || exit 1
fi

clean_unused_docker_compose || return 1