#!/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.


#:-
. /etc/shlib
#:-

include pretty
include parse

md5_compat() { md5sum | cut -c -32; }

depends shyaml docker

[[ "${BASH_SOURCE[0]}" != "${0}" ]] && SOURCED=true


export CACHEDIR=/var/cache/compose
export VARDIR=/var/lib/compose

mkdir -p "$CACHEDIR" || exit 1
 
trap_add "EXIT" clean_cache

usage="$exname CHARM"'

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
##

_merge_yaml_common_code="
import sys
import yaml


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

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

yaml.add_constructor(
    yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
    lambda cls, node: MyOrderedDict(cls.construct_pairs(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('%s. Conflicting key is %r. Values are:\n%s\n' % (e.args[0], e.args[1], e.args[2]))
        exit(1)
    if c is not None:
        print '%s' % yaml.dump(c, default_flow_style=False)


"


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)) 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"

$_merge_yaml_common_code

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

    python "$state_tmpdir/merge_yaml_str.py" "$@"
}
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]):
    yaml.load(sys.argv[2])}, default_flow_style=False)

EOF
    fi

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


##
## Functions
##

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

gen_password() {
    python -c 'import random; \
        xx = "azertyuiopqsdfghjklmwxcvbn1234567890AZERTYUIOPQSDFGHJKLMWXCVBN+_-"; \
        print "".join([xx[random.randint(0, len(xx)-1)] for x in range(0, 14)])'
}
export -f gen_password


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


get_docker_compose_links() {
    local service="$1" cache_file="$state_tmpdir/$FUNCNAME.cache.$1" \
          links charm charm_part master_charm
    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_charm=$(get_top_master_charm_for_service "$service") || return 1

    deps=()
    while read-0 relation_name target_service relation_config tech_dep; do
        master_target_charm="$(get_top_master_charm_for_service "$target_service")"
        [ "$master_charm" == "$master_target_charm" ] && continue
        if [ "$tech_dep" == "reversed" ]; then
            deps+=("$(echo -en "$master_target_charm:\n  links:\n    - $master_charm")")
        elif [ "$tech_dep" == "True" ]; then
            deps+=("$(echo -en "$master_charm:\n  links:\n    - $master_target_charm")")
        fi
    done < <(get_compose_relations "$service") || return 1

    merge_yaml_str "${deps[@]}" | tee "$cache_file"
}


_get_docker_compose_opts() {
    local service="$1" cache_file="$state_tmpdir/$FUNCNAME.cache.$1" \
          links charm charm_part master_charm
    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_charm="$(get_top_master_charm_for_service "$service")"
    docker_compose_opts=$(echo "$compose_def" | shyaml get-value "docker-compose" 2>/dev/null)

    if [ "$docker_compose_opts" ]; then
        yaml_key_val_str "$master_charm" "$docker_compose_opts"
    fi | tee "$cache_file"
}


##
## 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 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_charm=$(get_top_master_charm_for_service "$service") || return 1

    ## The compose part

    links_yaml=$(get_docker_compose_links "$service") || return 1
    docker_compose_options=$(_get_docker_compose_opts "$service") || return 1

    ## the charm part

    #debug "Get charm name from service name $DARKYELLOW$service$NORMAL."
    charm=$(get_service_charm "$service") || return 1
    charm_part=$(get_docker_compose_mixin_from_metadata "$charm") || return 1

    ## Merge results
    if [ "$charm_part" ]; then
        charm_yaml="$(yaml_key_val_str "$master_charm" "$charm_part")" || return 1
        merge_yaml_str "$links_yaml" "$charm_yaml" "$docker_compose_options" || return 1
    else
        echo "$links_yaml"
    fi | tee "$cache_file"
}
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
    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.conf' base for $DARKYELLOW$@$NORMAL..."
    for target_service in "$@"; do
        start=$SECONDS
        services=$(get_ordered_service_dependencies "$target_service") || 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") || 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

    merge_yaml_str "${entries[@]}" > "$cache_file"
    export _current_docker_compose="$(cat "$cache_file")"
    echo "$_current_docker_compose"
    debug "  ..Compilation of base 'docker-compose.conf' done.$GRAY(in $((SECONDS - start_compilation))s)$NORMAL" || true
    # debug " ** ${WHITE}docker-compose.conf${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 0
    fi

    service_def_base="charm: $service"
    value=$(echo "$docker_compose" | shyaml get-value "$service" 2>/dev/null)
    merge_yaml <(echo "$service_def_base") <(echo "$value") | tee "$cache_file"
}
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 0
    fi

    [ -z "$service" ] && print_syntax_error "Missing service as first argument."

    docker_compose=$(cat "$COMPOSE_YML_FILE") || return 1
    result=$(_get_compose_service_def_cached "$service" "$docker_compose") || return 1
    echo "$result" | tee "$cache_file"
}
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 0
    fi
    charm=$(echo "$service_def" | shyaml get-value charm 2>/dev/null)
    if [ -z "$charm" ]; then
        err "Missing charm in service $DARKYELLOW$service$NORMAL definition."
        return 1
    fi
    echo "$charm" | tee "$cache_file"
}
export -f _get_service_charm_cached

get_service_charm () {
    local service="$1"
    if [ -z "$service" ]; then
        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 "$service" 2>/dev/null)
    if [ -z "$def" ]; then
        err "No definition for service $DARKYELLOW$service$NORMAL in compiled 'docker-compose.conf'."
        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_charm charm service_image service_build service_dockerfile
    if [ -e "$cache_file" ]; then
        # debug "$FUNCNAME: cache hit ($*)"
        cat "$cache_file"
        return 0
    fi
    master_charm="$(get_top_master_charm_for_service "$service")" || {
        err "Could not compute base charm for service $DARKYELLOW$service$NORMAL."
        return 1
    }
    service_def="$(get_service_def "$master_charm")" || {
        err "Could not get docker-compose service definition for $DARKYELLOW$master_charm$NORMAL."
        return 1
    }
    service_image=$(echo "$service_def" | shyaml get-value image 2>/dev/null)
    if [ "$?" != 0 ]; 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
        service_dockerfile="$CHARM_STORE/${service_build}/Dockerfile"
        if ! [ -e "$service_dockerfile" ]; then
            err "No Dockerfile found in '$service_dockerfile' location."
            return 1
        fi

        grep '^FROM' "$service_dockerfile" | xargs echo | cut -f 2 -d " "
    else
        echo "$service_image"
    fi | tee "$cache_file"

}
export -f service_base_docker_image

## XXXvlab: provided in shlib/common
# read-0() {
#     local eof
#     eof=
#     while [ "$1" ]; do
#         IFS='' read -r -d '' "$1" || eof=true
#         shift
#     done
#     test "$eof" != true
# }
# export -f read-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


cmd_on_base_image() {
    local charm="$1"
    shift
    base_image=$(service_base_docker_image "$charm") || return 1

    docker run -i --entrypoint /bin/bash "$base_image" -c "$*"
}
export -f cmd_on_base_image

cached_cmd_on_base_image() {
    local charm="$1" cache_file="$state_tmpdir/$FUNCNAME.cache.$(echo "$*" | md5_compat)"
    shift
    if [ -e "$cache_file" ]; then
        # debug "$FUNCNAME: cache hit ($*)"
        cat "$cache_file"
        return 0
    fi
    base_image=$(service_base_docker_image "$charm") || return 1
    result=$(cmd_on_base_image "$charm" "$@") || return 1
    echo "$result" | tee "$cache_file"
}
export -f cached_cmd_on_base_image

array_values_to_stdin() {
    local e
    if [ "$#" -ne "1" ]; then
        print_syntax_warning "$FUNCNAME: need one argument."
        return 1
    fi
    var="$1"
    eval "for e in \"\${$var[@]}\"; do echo -en \"\$e\\0\"; done"
}

array_keys_to_stdin() {
    local e
    if [ "$#" -ne "1" ]; then
        print_syntax_warning "$FUNCNAME: need one argument."
        return 1
    fi
    var="$1"
    eval "for e in \"\${!$var[@]}\"; do echo -en \"\$e\\0\"; done"
}

array_kv_to_stdin() {
    local e
    if [ "$#" -ne "1" ]; then
        print_syntax_warning "$FUNCNAME: need one argument."
        return 1
    fi
    var="$1"
    eval "for e in \"\${!$var[@]}\"; do echo -n \"\$e\"; echo -en '\0'; echo -n \"\${$var[\$e]}\"; echo -en '\0'; done"
}


array_pop() {
    local narr="$1" nres="$2"
    for key in $(eval "echo \${!$narr[@]}"); do
        eval "$nres=\${$narr[\"\$key\"]}"
        eval "unset $narr[\"\$key\"]"
        return 0
    done
}
export -f array_pop

array_member() {
    local src elt
    src="$1"
    elt="$2"
    while read-0 key; do
        if [ "$(eval "echo -n \"\${$src[\$key]}\"")" == "$elt" ]; then
            return 0
        fi
    done < <(array_keys_to_stdin "$src")
    return 1
}
export -f array_member


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="$(get_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 metadata 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}
    echo "$value" | tee "$cache_file"
}
export -f get_charm_tech_dep_orientation_for_relation

##
## 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_compose_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
        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 services="$1" action="$2" loaded

    declare -A loaded
    for service in $services; do
        for subservice in $(get_ordered_service_dependencies "$service"); 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
            TARGET_SCRIPT="$CHARM_STORE/$charm/hooks/$action"
            [ -e "$TARGET_SCRIPT" ] || continue

            PROJECT_NAME=$(get_default_project_name) || return 1
            Wrap -d "$YELLOW$action$NORMAL hook of charm $DARKYELLOW$charm$NORMAL" <<EOF || return 1
            cd "$CHARM_STORE/$charm"
            SERVICE_NAME=$subservice \
                        CHARM_NAME="$charm" \
                        PROJECT_NAME=$PROJECT_NAME \
                        DOCKER_BASE_IMAGE=$(service_base_docker_image "$charm") \
                        SERVICE_DATASTORE="$DATASTORE/$charm" \
                        SERVICE_CONFIGSTORE="$CONFIGSTORE/$charm" \
                        "$TARGET_SCRIPT"
EOF
            loaded[$subservice]=1
        done
    done
    return 0
}


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


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


relation-target-compose-get () {
    local key="$1"
    echo "$RELATION_BASE_COMPOSE_DEF" | shyaml get-value "options.$key" 2>/dev/null
    if [ "$?" != 0 ]; then
        err "The key $WHITE$key$NORMAL was not found in base service compose definition.."
        return 1
    fi
}
export -f relation-base-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" <(echo "$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" <(echo "$metadata")
}
export -f init-config-add


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=$(echo "$relation_name" | tr "-" "_" )-relation-joined
    script_name="hooks/${base_script_name}"

    [ ! -e "$CHARM_STORE/$target_charm/$script_name" -a ! -e "$CHARM_STORE/$charm/$script_name" ] &&
        return 0

    relation_dir=$(get_relation_data_dir "$charm" "$target_charm" "$relation_name") || return 1
    RELATION_DATA_FILE=$(get_relation_data_file "$charm" "$target_charm" "$relation_name" "$relation_config") || return 1
    RELATION_BASE_COMPOSE_DEF=$(get_compose_service_def "$service") || return 1
    RELATION_TARGET_COMPOSE_DEF=$(get_compose_service_def "$target_service") || return 1
    export BASE_SERVICE_NAME=$service
    export TARGET_SERVICE_NAME=$target_service
    export BASE_CHARM_NAME=$charm
    export TARGET_CHARM_NAME=$target_charm
    PROJECT_NAME=$(get_default_project_name) || return 1
    MASTER_BASE_CHARM_NAME=$(get_top_master_charm_for_service "$service") || return 1
    MASTER_TARGET_CHARM_NAME=$(get_top_master_charm_for_service "$target_service") || return 1
    export RELATION_DATA_FILE RELATION_BASE_COMPOSE_DEF RELATION_TARGET_COMPOSE_DEF
    export MASTER_BASE_CHARM_NAME MASTER_TARGET_CHARM_NAME PROJECT_NAME
    target_errlvl=0
    if ! [ -e "$CHARM_STORE/$target_charm/$script_name" ]; then
        verb "No relation script '$script_name' in $DARKYELLOW$target_charm$NORMAL."
    else
        verb "Running ${DARKBLUE}$relation_name${NORMAL} relation-joined script" \
             "for target charm $DARKYELLOW$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
        {
            (
                cd "$CHARM_STORE/$target_charm"
                SERVICE_NAME=$target_service
                SERVICE_DATASTORE="$DATASTORE/$target_charm"
                SERVICE_CONFIGSTORE="$CONFIGSTORE/$target_charm"
                export SERVICE_NAME DOCKER_BASE_IMAGE SERVICE_DATASTORE SERVICE_CONFIGSTORE
                "$script_name"
                echo "$?" > "$relation_dir/target_errlvl"
            ) | logstdout "$DARKYELLOW$target_charm$NORMAL/$DARKBLUE$relation_name$NORMAL (joined) ${GREEN}@${NORMAL}"
        } 3>&1 1>&2 2>&3 | logstderr "$DARKYELLOW$target_charm$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 $DARKYELLOW$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 [ -e "$CHARM_STORE/$charm/$script_name" ]; then
            verb "Running ${DARKBLUE}$relation_name${NORMAL} relation-joined script" \
                 "for charm $DARKYELLOW$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
            {
                (
                    cd "$CHARM_STORE/$charm"
                    SERVICE_NAME=$service
                    SERVICE_DATASTORE="$DATASTORE/$charm"
                    SERVICE_CONFIGSTORE="$CONFIGSTORE/$charm"
                    export SERVICE_NAME DOCKER_BASE_IMAGE SERVICE_DATASTORE SERVICE_CONFIGSTORE
                    "$script_name"
                    echo "$?" > "$relation_dir/errlvl"
                ) | logstdout "$DARKYELLOW$charm$NORMAL/$DARKBLUE$relation_name$NORMAL (joined) ${GREEN}@${NORMAL}"
            } 3>&1 1>&2 2>&3 | logstderr "$DARKYELLOW$charm$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 $DARKYELLOW$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$charm$NORMAL failed to run properly."
            fi
        else
            verb "No relation script '$script_name' in charm $DARKYELLOW$charm$NORMAL. Ignoring."
        fi
    else
        err "Relation $DARKBLUE$relation_name$NORMAL on $DARKYELLOW$target_charm$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 0
    fi

    (
        set -o pipefail
        if [ "$compose_service_def" ]; then
            while read-0 relation_name relation_def; do
                (
                    case "$(echo "$relation_def" | shyaml get-type 2>/dev/null)" in
                    "str")
                        target_service="$(echo "$relation_def" | shyaml get-value 2>/dev/null)"
                        tech_dep="$(get_charm_tech_dep_orientation_for_relation "$target_service" "$relation_name")"
                        echo -en "$relation_name\0$target_service\0\0$tech_dep\0"
                        ;;
                    "sequence")
                        while read-0 target_service; do
                            tech_dep="$(get_charm_tech_dep_orientation_for_relation "$target_service" "$relation_name")"
                            echo -en "$relation_name\0$target_service\0\0$tech_dep\0"
                        done < <(echo "$relation_def" | shyaml get-values-0 2>/dev/null)
                        ;;
                    "struct")
                        while read-0 target_service relation_config; do
                            tech_dep="$(get_charm_tech_dep_orientation_for_relation "$target_service" "$relation_name")"
                            echo -en "$relation_name\0$target_service\0$relation_config\0$tech_dep\0"
                        done < <(echo "$relation_def" | shyaml key-values-0 2>/dev/null)
                        ;;
                    esac
                ) </dev/null >> "$cache_file"
            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
        err "Error while looking for compose relations."
        rm -f "$cache_file"  ## no cache
        return 1
    fi
    cat "$cache_file"
}
export -f get_compose_relations

run_service_relations () {
    local services="$1" loaded
    declare -A loaded
    for service in $(get_ordered_service_dependencies $services); do
        # debug "Upping dep's relations of ${DARKYELLOW}$service${NORMAL}:"
        for subservice in $(get_service_deps "$service") "$service"; do
            [ "${loaded[$subservice]}" ] && continue
            # debug "  Relations of ${DARKYELLOW}$subservice${NORMAL}:"
            while read-0 relation_name target_service relation_config tech_dep; do
                export relation_config
                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_compose_relations "$subservice") || return 1
            loaded[$subservice]=1
        done
    done
}
export -f run_service_relations


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

    read-0 charm script || 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

    compose_file=$(get_compose_yml_location) || return 1
    export action_errlvl_file="$state_tmpdir/action-$service-$charm-$action-errlvl"
    export state_tmpdir
    {
        (
            set +e  ## Prevents unwanted leaks from parent shell
            cd "$CHARM_STORE/$charm"
            export COMPOSE_CONFIG=$(cat "$compose_file")
            export METADATA_CONFIG=$(cat "$CHARM_STORE/$charm/metadata.yml")
            export SERVICE_NAME=$service
            export ACTION_NAME=$action
            export CONTAINER_NAME=$(get_top_master_charm_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 "$script" "$@"
            echo "$?" > "$action_errlvl_file"
        ) | logstdout "$DARKYELLOW$charm$NORMAL/${DARKCYAN}$action${NORMAL} ${GREEN}@${NORMAL}"
    } 3>&1 1>&2 2>&3 | logstderr "$DARKYELLOW$charm$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 target_script relation_config _dummy
    shift; shift

    read-0 charm target_charm relation_name target_script 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 "$charm" "$target_charm" "$relation_name" "$relation_config")

    compose_file=$(get_compose_yml_location) || return 1
    export action_errlvl_file="$state_tmpdir/action-$service-$charm-$action-errlvl"
    export state_tmpdir
    {
        (
            set +e  ## Prevents unwanted leaks from parent shell
            cd "$CHARM_STORE/$charm"
            export METADATA_CONFIG=$(cat "$CHARM_STORE/$charm/metadata.yml")
            export SERVICE_NAME=$service
            export RELATION_TARGET_CHARM="$target_charm"
            export RELATION_CHARM="$charm"
            export ACTION_NAME=$action
            export CONTAINER_NAME=$(get_top_master_charm_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 "$target_script" "$@"
            echo "$?" > "$action_errlvl_file"
        ) | logstdout "$DARKYELLOW$charm$NORMAL/${DARKCYAN}$action${NORMAL} ${GREEN}@${NORMAL}"
    } 3>&1 1>&2 2>&3 | logstderr "$DARKYELLOW$charm$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 charm="$1" target_charm="$2" relation_name="$3" cache_file="$state_tmpdir/$FUNCNAME.cache.$1"
    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/$charm-$target_charm/$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 charm="$1" target_charm="$2" relation_name="$3" relation_config="$4"

    relation_dir=$(get_relation_data_dir "$charm" "$target_charm" "$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" \
          target_script charm target_charm
    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 ?

    target_script="$CHARM_STORE/$charm/actions/$action"
    if [ -x "$target_script" ]; then
        echo -en "direct\0$charm\0$target_script" | 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
        target_script="$CHARM_STORE/$target_charm/actions/relations/$relation_name/$action"
        if [ -x "$target_script" ]; then
            echo -en "relation\0$charm\0$target_charm\0$relation_name\0$target_script\0$relation_config" | tee "$cache_file"
            return 0
        fi
    done < <(get_compose_relations "$service")

    master=$(get_top_master_charm_for_service "$charm")
    [ "$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 '$CHARM_STORE/$charm/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_master_charm_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
    if [ -e "$cache_file" ]; then
        # debug "$FUNCNAME: STATIC cache hit ($1)"
        cat "$cache_file"
        touch "$cache_file"
        return 0
    fi

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

    ## fetch the container relation
    requires="$(echo "$metadata" | shyaml get-value "requires" 2>/dev/null)"
    if [ -z "$requires" ]; then
        die "Charm $DARKYELLOW$charm$NORMAL is a subordinate but does not have any 'requires' " \
            "section."
    fi
    master_charm=
    while read-0 relation_name relation; do
        if [ "$(echo "$relation" | shyaml get-value "scope" 2>/dev/null)" == "container" ]; then
            # debug "$DARKYELLOW$service$NORMAL's relation" \
            #       "$DARKBLUE${relation_name}$NORMAL is a container."
            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_compose_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_charm=$(get_service_charm "$target_service") || return 1
            break
        fi
    done < <(echo "$requires" | shyaml key-values-0 2>/dev/null)
    if [ -z "$master_charm" ]; then
        die "Charm $DARKYELLOW$charm$NORMAL is a subordinate but does not have any relation with" \
            " ${WHITE}scope${NORMAL} set to 'container'."
    fi
    echo "$master_charm" | tee "$cache_file"
}
export -f _get_master_charm_for_service_cached

get_master_charm_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 0
    fi

    charm=$(get_service_charm "$service") || return 1
    metadata=$(get_charm_metadata "$charm") || return 1
    result=$(_get_master_charm_for_service_cached "$service" "$charm" "$metadata") || return 1
    echo "$result" | tee "$cache_file"
}
export -f get_master_charm_for_service


get_top_master_charm_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_charm_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_charm_for_service


get_charm_metadata() {
    local charm="$1" metadata_file
    metadata_file="$CHARM_STORE/$charm/metadata.yml"
    if ! [ -e "$metadata_file" ]; then
        return 0  ## No metadata file is as if metadata was empty
    fi

    cat "$metadata_file"
}
export -f get_charm_metadata

##
## 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 charm="$1" metadata="$2" cache_file="$CACHEDIR/$FUNCNAME.cache.$(echo "$*" | md5_compat)" \
          metadata_file metadata volumes docker_compose subordinate image
    if [ -e "$cache_file" ]; then
        #debug "$FUNCNAME: STATIC cache hit $1"
        cat "$cache_file"
        touch "$cache_file"
        return 0
    fi

    mixin=
    if [ "$metadata" ]; then
        ## resources to volumes
        volumes=$(
            for resource_type in data config; do
                while read-0 resource; do
                    eval "echo \"  - \$${resource_type^^}STORE/\$charm\$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:rw"
                else
                    echo "  - $resource:$resource:rw"
                fi
            done < <(echo "$metadata" | shyaml get-values-0 "host-resources" 2>/dev/null)
            while read-0 resource; do
                echo "  - $CHARM_STORE/$charm/resources$resource:$resource:rw"
            done < <(echo "$metadata" | shyaml get-values-0 "charm-resources" 2>/dev/null)
               ) || return 1
        if [ "$volumes" ]; then
            mixin=$(echo -en "volumes:\n$volumes")
        fi

        docker_compose=$(echo "$metadata" | shyaml get-value "docker-compose" 2>/dev/null)
        if [ "$docker_compose" ]; then
            mixin=$(merge_yaml_str "$mixin" "$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_or_build_statement=
    if [ "$image" ]; then
        if [ "$subordinate" ]; then
            err "Subordinate charm can not have a ${WHITE}docker-image${NORMAL} value."
            return 1
        fi
        image_or_build_statement="image: $image"
    elif [ -d "$CHARM_STORE/$charm/build" ]; then
        if [ "$subordinate" ]; then
            err "Subordinate charm can not have a 'build' sub directory."
            return 1
        fi
        image_or_build_statement="build: $charm/build"
    fi
    if [ "$image_or_build_statement" ]; then
        mixin=$(merge_yaml_str "$mixin" "$image_or_build_statement")
    fi
    echo "$mixin" | tee "$cache_file"
}
export -f _get_docker_compose_mixin_from_metadata_cached


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

    metadata="$(get_charm_metadata "$charm")" || return 1
    mixin=$(_get_docker_compose_mixin_from_metadata_cached "$charm" "$metadata") || 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)"
    if [ "$compose_yml_location" ]; then
        if normalized_path=$(readlink -e "$compose_yml_location"); then
            echo "$(basename "$(dirname "$normalized_path")")"
            return 0
        fi
    fi
    echo "project"
    return 0
}
export -f get_default_project_name


launch_docker_compose() {

    docker_compose_tmpdir=$(mktemp -d -t tmp.XXXXXXXXXX)
    #debug "Creating temporary docker-compose directory in '$docker_compose_tmpdir'."
#     trap_add EXIT "debug \"Removing temporary docker-compose directory in $docker_compose_tmpdir.\";\
# rm -rf \"$docker_compose_tmpdir\""
    trap_add EXIT "rm -rf \"$docker_compose_tmpdir\""
    project=$(get_default_project_name)
    mkdir -p "$docker_compose_tmpdir/$project"
    docker_compose_dir="$docker_compose_tmpdir/$project"

    if [ -z "$SERVICE_PACK" ]; then
        export SERVICE_PACK=$(get_default_target_services $SERVICE_PACK)
    fi
    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

    ## XXXvlab: could be more specific and only link the needed charms
    ln -sf "$CHARM_STORE/"* "$docker_compose_dir/"
    mkdir "$docker_compose_dir/.data"

    {
        {
            cd "$docker_compose_dir"
            debug "${WHITE}docker-compose.yml$NORMAL for $DARKYELLOW$SERVICE_PACK$NORMAL"
            debug "$(cat "$docker_compose_dir/docker-compose.yml")"
            debug "${WHITE}Launching$NORMAL: docker-compose $@"
            docker-compose "$@"
            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")"
    if [ -z "docker_compose_dir" ]; 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() {
    parent=$(while ! [ -e "./compose.yml" ]; do
                 [ "$PWD" == "/" ] && exit 0
                 cd ..
             done; echo "$PWD"
          )
    if [ "$parent" ]; then
        echo "$parent/compose.yml"
        return 0
    fi
    if [ "$DEFAULT_COMPOSE_FILE" ]; then
        if ! [ -e "$DEFAULT_COMPOSE_FILE" ]; then
            err "No 'compose.yml' was found in current or parent dirs," \
                "and \$DEFAULT_COMPOSE_FILE points to an unexistent file."
            die "Please provide a 'compose.yml' file."
        fi
        echo "$DEFAULT_COMPOSE_FILE"
        return 0
    fi
    err "No 'compose.yml' was found in current or parent dirs, and no \$DEFAULT_COMPOSE_FILE was set."
    die "Please provide a 'compose.yml' file."
    return 1
}
export -f get_compose_yml_location


get_default_target_services() {
    local services=("$@")
    if [ -z "${services[*]}"  ]; then
        if [ "$DEFAULT_SERVICES" ]; then
            info "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
    declare -A loaded
    for service in "$@"; do
        master_service=$(get_top_master_charm_for_service "$service") || return 1
        if [ "${loaded[$master_service]}" ]; then
            continue
        fi
        echo "$master_service"
        loaded["$master_service"]=1
    done | xargs echo
}
export -f get_master_services


_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_opts_list() {
    local cache_file="$state_tmpdir/$FUNCNAME.cache.$(echo "$*" | md5_compat)"
    if [ -e "$cache_file" ]; then
        debug "$FUNCNAME: cache hit ($*)"
        cat "$cache_file"
        return 0
    fi
    docker-compose "$@" --help | grep '^Options:' -A 20000 |
        tail -n +2 |
        egrep "^\s+-" |
        sed -r 's/\s+((((-[a-zA-Z]|--[a-zA-Z0-9-]+)( [A-Z=]+|=[^ ]+)?)(, )?)+)\s+.*$/\1/g' |
        tee "$cache_file"
}

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

get_docker_compose_multi_opts_list() {
    opts_list=$(get_docker_compose_opts_list "$@")
    echo "$opts_list" | egrep "$_MULTIOPTION_REGEX_LINE_FILTER" |
        sed -r "s/^($_MULTIOPTION_REGEX)(\s|=).*$/\1/g" |
        tr ',' "\n" | xargs echo
}

get_docker_compose_single_opts_list() {
    opts_list=$(get_docker_compose_opts_list "$@")
    echo "$opts_list" | egrep -v "$_MULTIOPTION_REGEX_LINE_FILTER" |
        tr ',' "\n" | xargs echo
}

clean_cache() {
    local i=0
    for f in $(ls -t "$CACHEDIR/"*.cache.* | 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
}

[ "$SOURCED" ] && return 0


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 ?
    . /etc/compose.conf
    . /etc/compose.local.conf
fi

_setup_state_dir

##
## Argument parsing
##

remainder_args=()
compose_opts=()
action_opts=()
no_hooks=
no_init=
action=
stage="main"  ## switches from 'main', to 'action', 'remainder'
# DC_MATCH_MULTI=
# DC_MATCH_SINGLE=
while [ "$#" != 0 ]; do
    case "$stage" in
        "main")
            case "$1" in
                --help|-h)
                    no_init=true ; no_hooks=true ; no_relations=true
                    compose_opts+=("$1")
                    ;;
                --verbose|-v)
                    export VERBOSE=true
                    compose_opts+=("$1")
                    ;;
                -f)
                    [ -e "$2" ] || die "File $2 doesn't exists"
                    export DEFAULT_COMPOSE_FILE="$2"
                    shift
                    ;;
                -p)
                    export DEFAULT_PROJECT_NAME="$2"
                    shift
                    ;;
                --no-relations)
                    export no_relations=true
                    ;;
                --no-hooks)
                    export no_hooks=true
                    ;;
                --no-init)
                    export no_init=true
                    ;;
                --debug)
                    export DEBUG=true
                    export VERBOSE=true
                    ;;
                --*|-*)
                    compose_opts+=("$1")
                    ;;
                *)
                    action="$1"
                    stage="action"
                    # DC_MATCH_MULTI=$(get_docker_compose_multi_opts_list "$action") || return 1
                    # DC_MATCH_SINGLE="$(get_docker_compose_single_opts_list "$action") $(echo "$DC_MATCH_MULTI" | sed -r 's/( |$)/=\* /g')"
                    ;;
            esac
            ;;
        "action")
            case "$1" in
                --help|-h)
                    no_init=true ; no_hooks=true ; no_relations=true
                    action_opts+=("$1")
                    ;;
                --verbose|-v)
                    export VERBOSE=true
                    action_opts+=("$1")
                    ;;
                --*|-*)
                    action_opts+=("$1")
                    ;;
                *)
                    action_posargs+=("$1")
                    stage="remainder"
                    ;;
            esac
            ;;
        "remainder")
            remainder_args+=("$@")
            break 3;;
    esac
    shift
done

[ "${compose_opts[*]}" ] && debug "Main docker-compose opts: ${compose_opts[*]}"
[ "${action_opts[*]}" ] && debug "Action '$action' opts: ${action_opts[*]}"
[ "${remainder_args[*]}" ] && debug "Remainder args: ${remainder_args[*]}"


##
## Actual code
##

export CHARM_STORE=${CHARM_STORE:-/srv/charm-store}
export DOCKER_DATASTORE=${DOCKER_DATASTORE:-/srv/docker-datastore}

export COMPOSE_YML_FILE=$(get_compose_yml_location) || exit 1
debug "Found 'compose.yml' file in '$COMPOSE_YML_FILE'."

if ! [ -d "$CHARM_STORE" ]; then
    err "Charm store path $YELLOW$CHARM_STORE$NORMAL does not exists. "
    err "Please check your $YELLOW\$CHARM_STORE$NORMAL variable value."
    exit 1
fi


if [ -z "$(cd "$CHARM_STORE"; ls)" ]; then
    err "no available charms in charm store $YELLOW$CHARM_STORE$NORMAL. Either:"
    err "  - check $YELLOW\$CHARM_STORE$NORMAL variable value"
    err "  - download charms in $CHARM_STORE"
    print_error "Charm store is empty. Cannot continue."
fi

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

is_service_action=
case "$action" in
    up|build|start|stop|config)
        services="$(get_default_target_services "${action_posargs[@]}")" || exit 1
        orig_services="${action_posargs[@]:1}"
        ;;
    run)
        services="${action_posargs[0]}"
        ;;
    "")
        services=
        ;;
    *)
        if is_service_action=$(has_service_action "${action_posargs[0]}" "$action"); then
            debug "Found action $DARKCYAN$action$NORMAL in service $DARKYELLOW${action_posargs[0]}$NORMAL"
            services="${action_posargs[0]}"
        else
            services="$(get_default_target_services "${action_posargs[@]}")"
        fi
        ;;
esac


get_docker_compose $services >/dev/null || {  ## precalculate variable \$_current_docker_compose
    err "Fails to compile base 'docker-compose.conf'"
    exit 1
}

##
## Pre-action
##


full_init=
case "$action" in
    up|run)
        full_init=true
        ;;
    "")
        full_init=
        ;;
    *)
        if [ "$is_service_action" ]; then
            full_init=true
        fi
        ;;
esac

if [ "$full_init" ]; then
    ## init in order
    Section initialisation
    if [ -z "$no_init" ]; then
        run_service_hook "$services" init || exit 1
    fi

    ## Get relations
    if [ -z "$no_relations" ]; then
        run_service_relations "$services" || exit 1
    fi

    ## XXXvlab: to be removed when all relation and service stuff is resolved
    if [ -z "$no_hooks" ]; then
        ordered_services=$(get_ordered_service_dependencies $services) || exit 1
        for service in $ordered_services; do
            charm=$(get_service_charm "$service") || exit 1
            for script in "$CHARM_STORE/$charm/hooks.d/"*.sh; do
                [ -e "$script" ] || continue
                [ -x "$script" ] || { echo "compose: script $script is not executable." >&2; exit 1; }
                (
                    debug "Launching '$script'."
                    cd "$(dirname "$script)")";
                    "$script" "$@"
                ) || { echo "compose: hook $script failed. Stopping." >&2; exit 1; }
            done
        done
    fi
fi

export SERVICE_PACK="$services"

##
## Docker-compose
##

case "$action" in
    up|start|stop|build)
        master_services=$(get_master_services $SERVICE_PACK) || exit 1
        launch_docker_compose "${compose_opts[@]}" "$action" "${action_opts[@]}" $master_services
        ;;
    run)
        master_service=$(get_master_services $SERVICE_PACK) || exit 1
        launch_docker_compose "${compose_opts[@]}" "$action" "${action_opts[@]}" "$master_service" "${remainder_args[@]}"
        ;;
    # enter)
    #     master_service=$(get_master_services $SERVICE_PACK) || exit 1
    #     [ "${remainder_args[*]}" ] || remainder_args=("/bin/bash" "-c" "export TERM=xterm; exec bash")
    #     docker exec -ti "${action_opts[@]}" "$master_service" "${remainder_args[@]}"
    #     ;;
    config)
        ## removing the services
        launch_docker_compose "${compose_opts[@]}" "$action" "${action_opts[@]}" "${remainder_args[@]}"
        warn "Runtime configuration modification (from relations) are not included here."
        ;;
    *)
        if [ "$is_service_action" ]; then
            run_service_action "$SERVICE_PACK" "$action" "${remainder_args[@]}"
        else
            launch_docker_compose "${compose_opts[@]}" "$action" "${action_opts[@]}" "${remainder_args[@]}"
        fi
        ;;
esac