From 110ffae47db27a49bbc43f86ba3737bccc1b3085 Mon Sep 17 00:00:00 2001 From: Trygve Laugstøl Date: Sat, 26 Jan 2013 23:58:22 +0100 Subject: o Rewriting most of this stuff to make it feel more like git. --- .app/lib/app-common | 160 ------------- .app/lib/app-conf | 205 ---------------- .app/lib/app-instance | 496 --------------------------------------- .app/lib/app-operate | 67 ------ .app/lib/pid-method | 113 --------- Makefile | 14 ++ NOTES.tmp | 16 ++ README.md | 166 +++++++++++++ app | 131 +++-------- bin/app-conf | 135 +++++++++++ bin/app-init | 79 +++++++ bin/app-instance | 502 ++++++++++++++++++++++++++++++++++++++++ bin/app-operate | 73 ++++++ bin/pid-method | 113 +++++++++ docs/README.md | 166 ------------- lib/common | 168 ++++++++++++++ lib/default-config | 1 + libexec/app-cat-conf | 47 ++++ libexec/app-grep-path | 13 ++ libexec/app-install-file | 172 ++++++++++++++ libexec/app-resolver-maven | 231 ++++++++++++++++++ test/app-cat-conf.bats | 56 +++++ test/app-common.bats | 14 ++ test/app-conf.bats | 165 +++++++------ test/app-init.bats | 42 ++++ test/app-install.bats | 9 +- test/data/app-cat-conf/config-1 | 7 + test/data/app-cat-conf/config-2 | 1 + test/data/app-cat-conf/config-3 | 4 + test/data/app-common/app-bar | 0 test/data/app-common/app-faz | 0 test/data/app-common/app-foo | 0 test/utils.bash | 78 +++++-- 33 files changed, 2054 insertions(+), 1390 deletions(-) delete mode 100644 .app/lib/app-common delete mode 100644 .app/lib/app-conf delete mode 100644 .app/lib/app-instance delete mode 100644 .app/lib/app-operate delete mode 100755 .app/lib/pid-method create mode 100644 Makefile create mode 100644 NOTES.tmp create mode 100644 README.md create mode 100755 bin/app-conf create mode 100755 bin/app-init create mode 100755 bin/app-instance create mode 100755 bin/app-operate create mode 100755 bin/pid-method delete mode 100644 docs/README.md create mode 100644 lib/common create mode 100644 lib/default-config create mode 100755 libexec/app-cat-conf create mode 100755 libexec/app-grep-path create mode 100755 libexec/app-install-file create mode 100755 libexec/app-resolver-maven create mode 100755 test/app-cat-conf.bats create mode 100755 test/app-common.bats create mode 100755 test/app-init.bats create mode 100644 test/data/app-cat-conf/config-1 create mode 100644 test/data/app-cat-conf/config-2 create mode 100644 test/data/app-cat-conf/config-3 create mode 100755 test/data/app-common/app-bar create mode 100755 test/data/app-common/app-faz create mode 100755 test/data/app-common/app-foo diff --git a/.app/lib/app-common b/.app/lib/app-common deleted file mode 100644 index fabdad4..0000000 --- a/.app/lib/app-common +++ /dev/null @@ -1,160 +0,0 @@ -#!/bin/bash - -assert_is_instance() { - local usage=$1 - local name=$2 - local instance=$3 - local check_link=$4 - - if [ -z "$name" ] - then - $usage "Missing required option -n." - fi - - if [ -z "$instance" ] - then - $usage "Missing required option -i." - fi - - # Use $APPSH_APPS as prefix if it's set - local x="" - if [ ! -z "$APPSH_APPS" ] - then - x="$APPSH_APPS/" - fi - - if [ ! -d $x$name/$instance ] - then - echo "No such instance: $name/$instance." >&2 - exit 1 - fi - - if [ "$check_link" != "no" ] - then - if [ ! -e $x$name/$instance/current ] - then - echo "Missing 'current' link." >&2 - exit 1 - fi - fi -} - -list_apps() { - filter_name=$1; shift - filter_instnace=$1; shift - vars="$@" - - sort $apps/.app/var/list | while read line - do - echo $line | (IFS=:; while read name instance version junk - do - if [ -n "$filter_name" -a "$filter_name" != "$name" ] - then - continue - fi - - if [ -n "$filter_instance" -a "$filter_instance" != "$instance" ] - then - continue - fi - - local line="" - IFS=" "; for var in $vars - do - case $var in - name) x=$name;; - instance) x=$instance;; - version) x=$version;; - current_version) x=`find_current_version $name $instance`;; - *) x="";; - esac - - if [ -z "$line" ] - then - line="$line$x" - else - line="$line:$x" - fi - done - echo $line - done) - done -} - -find_current_version() { - name=$1 - instance=$2 - - if [ ! -L $apps/$name/$instance/current ] - then - return 0 - fi - - ( - cd $apps/$name/$instance - ls -l current | sed -n "s,.* current -> versions/\(.*\)/root,\1,p" - ) -} - -find_versions() { - name=$1 - instance=$2 - - if [ ! -d $apps/$name/$instance/versions ] - then - return 0 - fi - - ( - cd $apps/$name/$instance/versions - ls -1d * - ) -} - -# TODO: set ulimit -# TODO: set umask -# TODO: change group newgrp/sg -run_app() { - local name=$1; shift - local instance=$1; shift - local bin=$1; shift - local method=$1; shift - - assert_is_instance operate_usage "$name" "$instance" - - local x="" - if [ ! -z "$APPSH_APPS" ] - then - x="$APPSH_APPS/" - fi - - ( - cd $x$name/$instance - APPSH_INSTANCE_HOME=`pwd` - cd current - - e="`get_conf_in_group $apps $name $instance env`" - # This magically get the expansion of $u correct. - IFS=" -" - - # Set a default PATH which can be overridden by the application's settings - set +e - env -i \ - PATH=/bin:/usr/bin \ - $e \ - PWD=$PWD \ - APPSH_METHOD=$method \ - APPSH_APPS=$apps \ - APPSH_HOME=$APPSH_HOME \ - APPSH_NAME=$name \ - APPSH_INSTANCE=$instance \ - APPSH_INSTANCE_HOME=$APPSH_INSTANCE_HOME \ - $bin "$@" - local ret=$? - set +x - set -e - - exit $ret - ) -} diff --git a/.app/lib/app-conf b/.app/lib/app-conf deleted file mode 100644 index 8149e41..0000000 --- a/.app/lib/app-conf +++ /dev/null @@ -1,205 +0,0 @@ -#!/bin/bash - -# TODO: Add a 'get' method that returns a single value -# Exit with 0 if found, 1 otherwise. - -key_expr="[a-zA-Z][_a-zA-Z0-9]*\.[a-zA-Z][_a-zA-Z0-9]*" - -format_conf() { - local IFS== - while read key value - do - printf "%-20s %-20s" "$key" "$value" - echo - done -} - -get_conf() { - local apps=$1 - local name=$2 - local instance=$3 - local key=$4 - local default= - local file=$apps/$name/$instance/current/etc/app.conf - - shift 4 - - if [ $# -gt 0 ] - then - default=$1 - shift - fi - - if [ ! -r $file ] - then - echo "$default" - return 0 - fi - - value=`sed -n "s,^${key}[ ]*=[ ]*\(.*\)$,\1,p" $file` - - if [ -z "$value" ] - then - echo "$default" - fi - - echo "$value" -} - -get_conf_all() { - local apps=$1 - local name=$2 - local instance=$3 - local file=$apps/$name/$instance/current/etc/app.conf - - if [ ! -r "$file" ] - then - return 0 - fi - - sed -n "s,^[ ]*\($key_expr\)[ ]*=[ ]*\(.*\)$,\1=\2,p" "$file" | sort -} - -get_conf_in_group() { - local apps=$1 - local name=$2 - local instance=$3 - local group=$4 - - get_conf_all "$apps" "$name" "$instance" | sed -n "s,^${group}\.\(.*\),\1,p" -} - -assert_key() { - key=$1 - - local x=`echo $key | sed -n "/^$key_expr$/p"` - if [ -z "$x" ] - then - echo "Invalid key: $key" >&2 - exit 1 - fi -} - -conf_set() { - local apps=$1 - local name=$2 - local instance=$3 - local key=$4 - local value=$5 - - local file=$apps/$name/$instance/current/etc/app.conf - - assert_key "$key" - - if [ -r $file ] - then - sed "/^$key[ ]*=.*/d" $file > $file.tmp - fi - - echo "$key=$value" >> $file.tmp - mv $file.tmp $file -} - -conf_delete() { - local apps=$1 - local name=$2 - local instance=$3 - local key=$4 - - local file=$apps/$name/$instance/current/etc/app.conf - - assert_key "$key" - - sed "/^$key[ ]*=.*/d" $file > $file.tmp - mv $file.tmp $file -} - -method_conf_list() { - local name=$1; shift - local instance=$1; shift - - get_conf_all "$apps" "$name" "$instance" | format_conf -} - -method_conf_usage() { - if [ -n "$1" ] - then - echo "Error:" $@ >&2 - fi - - echo "usage: $0 conf " >&2 - echo "" - echo "Available methods:" >&2 - echo " list - list all configuration values" >&2 - echo " list-group [group] - list all configuration values in the specified group" >&2 - echo " set [group.key] [value] - set a configuration parameter" >&2 - echo " delete [group.key] - deletes a configuration parameter" >&2 - exit 1 -} - -method_conf() { - local name="$1"; shift - local instance="$1"; shift - local method="$1" - - if [ $# -gt 0 ] - then - shift - fi - - assert_is_instance method_conf_usage "$name" "$instance" - - case "$method" in - list) - if [ $# -gt 0 ] - then - method_conf_usage "Extra options." - exit 1 - fi - - method_conf_list "$name" "$instance" - ;; - list-group) - if [ $# -ne 1 ] - then - method_conf_usage - exit 1 - fi - - get_conf_in_group "$apps" "$name" "$instance" "$1" | format_conf - ;; - set) - if [ $# -ne 2 ] - then - method_conf_usage - exit 1 - fi - - conf_set "$apps" "$name" "$instance" "$1" "$2" - ;; - delete) - if [ $# -ne 1 ] - then - method_conf_usage - exit 1 - fi - - conf_delete "$apps" "$name" "$instance" "$1" - ;; - *) - if [ $# -eq 0 ] - then - method_conf_list "$name" "$instance" - exit 0 - fi - - if [ -z "$method" ] - then - method_conf_usage - else - method_conf_usage "Unknown method $method" - fi - exit 1 - ;; - esac -} diff --git a/.app/lib/app-instance b/.app/lib/app-instance deleted file mode 100644 index 25983f0..0000000 --- a/.app/lib/app-instance +++ /dev/null @@ -1,496 +0,0 @@ -#!/bin/bash - -if [ -n "$APPSH_REPO" ] -then - repo="$APPSH_REPO" -else - repo="http://repo1.maven.org" -fi - -calculate_md5() { - local file="$1"; shift - - md5sum "$file" | cut -c 1-32 -} - -# TODO: support file:// repositories -# TODO: look in the local repository first -get() { - local url=$1 - local file=$2 - local exit - - curl -o $file $url -D curl.tmp - - exit=`grep "^HTTP/[0-9]\.[0-9] 200 .*" curl.tmp >/dev/null; echo $?` - head=`head -n 1 curl.tmp` - rm -f curl.tmp - if [ "$exit" != 0 ] - then - echo "Unable to download $url: $head" >&2 - exit 1 - fi -} - -resolve_snapshot() { - local groupId=$1; shift - local groupIdSlash=$1; shift - local artifactId=$1; shift - local version=$1; shift - - local metadata=$apps/.app/var/download/$groupId-$artifactId-$version-metadata.xml - local base_url=$repo/$groupIdSlash/$artifactId/$version - get $base_url/maven-metadata.xml $metadata - local resolved_version=`xmlstarlet sel -t -m '//snapshotVersion[extension[text()="zip"]]' -v value $metadata` - echo $resolved_version -} - -download_artifact() { - local file="$1"; shift - local url="$1"; shift - - echo "Downloading $url.md5" - get $url.md5 $file.md5 - local expected_md5="`cat $file.md5`" - - if [ -r $file ] - then - if [ "$expected_md5" == "`calculate_md5 $file`" ] - then - echo "Artifact already downloaded." - else - rm -f "$file" - fi - return 0 - fi - echo "Downloading artifact: $url" - get $url $file - - local actual_md5="`calculate_md5 $file`" - if [ "$expected_md5" == "$actual_md5" ] - then - echo "Artifact downloaded." - else - echo "Invalid checksum. Expected $expected_md5, got $actual_md5" >&2 - exit 1 - fi -} - -method_install_usage() { - if [ -n "$1" ] - then - echo "Error:" "$@" >&2 - fi - - echo "usage: install <-r resolver> -u " >&2 - echo "" >&2 - echo "Install package from a Maven repository:" >&2 - echo " $0 [-n name] [-i instance] instance install -r maven -u groupId:artifactId:version" >&2 - echo "" >&2 - echo "Install package from a file:" >&2 - echo " $0 [-n name] [-i instance] instance install -r file -u file [-v version]" >&2 - echo "The version defaults to the current timestamp" >&2 - exit 1 -} - -method_install() { - local name="$1"; shift - local instance="$1"; shift - local version - local resolver - local url - local groupId - local artifactId - local zip_file - - - if [ $# -eq 0 ] - then - method_install_usage - fi - - while getopts "n:i:v:r:u:" opt - do - case $opt in - n) - name=$OPTARG - ;; - i) - instance=$OPTARG - ;; - v) - version=$OPTARG - ;; - r) - resolver=$OPTARG - ;; - u) - url=$OPTARG - ;; - \?) - method_install_usage "Invalid option: -$OPTARG" - ;; - esac - done - - if [ -z "$name" ] - then - method_install_usage "Missing required argument: -i name." - fi - - if [ -z "$instance" ] - then - method_install_usage "Missing required argument: -i instance." - fi - - if [ -z "$resolver" ] - then - method_install_usage "Missing required option: -r resolver" - fi - - if [ -z "$url" ] - then - method_install_usage "Missing required option: -u url" - fi - - case "$resolver" in - maven) - url=`echo $url | tr ":" " "`; set -- $url - groupId=$1 - artifactId=$2 - version=$3 - - if [ -z "$groupId" -o -z "$artifactId" -o -z "$version" ] - then - method_install_usage "Invalid Maven url." - fi - - local groupIdSlash=$(echo $groupId | sed "s,\.,/,g") - if [ "`echo $version | sed -n s,.*-SNAPSHOT$,SNAPSHOT,p`" == "SNAPSHOT" ] - then - echo "Resolving version $version..." - local resolved_version=`resolve_snapshot $groupId $groupIdSlash $artifactId $version` - if [ -z "$resolved_version" ] - then - echo "Unable to resolve version." - exit 1 - fi - echo "Resolved version $version to $resolved_version" - else - resolved_version=$version - fi - - zip_file=$apps/.app/var/download/$groupId-$artifactId-$resolved_version.zip - artifact_url=$repo/$groupIdSlash/$artifactId/$version/$artifactId-$resolved_version.zip - - download_artifact "$zip_file" "$artifact_url" - ;; - file) - if [ ! -r "$url" ] - then - echo "Could not read file: $url" >&2 - exit 1 - fi - - # TODO: should the zip file be copied into download/ so that - # there's always a local copy? - zip_file=$url - - if [ -z "$version" ] - then - version=`TZ=UTC date +"%Y%m%d-%H%M%S"` - fi - - resolved_version=$version - ;; - *) - method_install_usage "Invalid resolver type: $resolver" - ;; - esac - - if [ -d $name/$instance/versions/$resolved_version ] - then - echo "Version $resolved_version is already installed" - exit 1 - fi - - if [ ! -d $name/$instance ] - then - echo "Creating instance '$instance' for '$name'" - mkdir -p $name/$instance - fi - - mkdir -p $name/$instance/versions/$resolved_version - - echo "Unpacking..." - unzip -q -d $name/$instance/versions/$resolved_version $zip_file - - if [ ! -d $name/$instance/versions/$resolved_version/root ] - then - echo "Invalid zip file, did not contain a ./root directory." >&2 - exit 1 - fi - - echo "Changing current symlink" - rm -f $apps/$name/$instance/current - ln -s versions/$resolved_version/root $apps/$name/$instance/current - - if [ -d $name/$instance/current/bin ] - then - ( - cd $name/$instance/current - find bin -type f | xargs chmod +x - ) - fi - - ( - cd $name/$instance/versions/$resolved_version - if [ -d scripts ] - then - find scripts | xargs chmod +x - fi - - if [ -x scripts/postinstall ] - then - echo "Running postinstall..." - cd root - set +e - env -i \ - PATH=/bin:/usr/bin \ - APPSH_APPS=$apps \ - APPSH_HOME=$APPSH_HOME \ - APPSH_NAME=$name \ - APPSH_INSTANCE=$instance \ - APPSH_VERSION=$resolved_version \ - ../scripts/postinstall - set -e - ret=`echo $?` - if [ "$ret" != 0 ] - then - echo "Postinstall failed!" - exit 1 - fi - echo "Postinstall completed successfully" - fi - ) - - if [ -r $apps/.app/var/list ] - then - sed "/^$name:$instance/d" $apps/.app/var/list > $apps/.app/var/list.new - fi - echo "$name:$instance:$version:$url" >> $apps/.app/var/list.new - mv $apps/.app/var/list.new $apps/.app/var/list -} - -method_set_current_usage() { - if [ -n "$1" ] - then - echo "Error:" "$@" >&2 - fi - - echo "usage: set-current -v version" >&2 - exit 1 -} - -method_set_current() { - local name="$1"; shift - local instance="$1"; shift - local version - - if [ $# -eq 0 ] - then - method_set_current_usage - fi - - while getopts "n:i:v:" opt - do - case $opt in - n) - name=$OPTARG - ;; - i) - instance=$OPTARG - ;; - v) - version=$OPTARG - ;; - \?) - method_set_current_usage "Invalid option: -$OPTARG" - ;; - esac - done - - if [ -z "$version" ] - then - echo "Missing required option -v version." >&2 - exit 1 - fi - - assert_is_instance method_set_current_usage "$name" "$instance" "no" - - if [ ! -d $apps/$name/$instance/versions/$version ] - then - echo "Invalid version: $version." - exit 1 - fi - - rm -f $apps/$name/$instance/current - ln -s versions/$version/root $apps/$name/$instance/current - - return 0 -} - -method_list_usage() { - if [ -n "$1" ] - then - echo "Error:" "$@" >&2 - fi - - echo "usage: list [-n name] [-P field]" >&2 - echo "" - echo "List all installed applications:" >&2 - echo " list" >&2 - echo "" - echo "List all applications with the selected fields with parseable output:" >&2 - echo " list -P instance -P version -n foo" >&2 - exit 1 -} - -method_list() { - local filter_name="$1"; shift - local filter_instance="$1"; shift - local mode="pretty" - local vars - local filter_name - - while getopts "P:n:i:" opt - do - case $opt in - P) - mode="parseable" - vars="$vars $OPTARG" - ;; - n) - filter_name=$OPTARG - ;; - i) - filter_instance=$OPTARG - ;; - \?) - method_list_usage "Invalid option: -$OPTARG" - ;; - esac - done - - if [ ! -r $apps/.app/var/list ] - then - return - fi - - if [ $mode = "pretty" ] - then - printf "%-20s %-20s %-20s\n" "Name" "Instance" "Version" - list_apps "$filter_name" "$filter_instance" name instance version | (IFS=:; while read name instance version - do - printf "%-20s %-20s %-20s\n" "$name" "$instance" "$version" - done) - else - list_apps "$filter_name" "$filter_instance" $vars - fi -} - -method_list_versions_usage() { - if [ -n "$1" ] - then - echo "Error:" "$@" >&2 - fi - - echo "usage: list-versions -n name -i instance [-P]" >&2 - exit 1 -} - -method_list_versions() { - local filter_name="$1"; shift - local instance="$1"; shift - local version - local mode="pretty" - - if [ $# -eq 0 ] - then - method_list_versions_usage - fi - - while getopts "n:i:P" opt - do - case $opt in - n) - name=$OPTARG - ;; - i) - instance=$OPTARG - ;; - v) - version=$OPTARG - ;; - P) - mode="parseable" - ;; - \?) - method_list_versions_usage "Invalid option: -$OPTARG" - ;; - esac - done - - assert_is_instance method_list_versions_usage "$name" "$instance" "no" - - if [ $mode = "pretty" ] - then - echo "Available versions for $name/$instance:" - fi - - find_versions $name $instance - - return 0 -} - -method_instance_usage() { - if [ -n "$1" ] - then - echo "Error:" $@ >&2 - fi - - echo "usage: $0 instance " >&2 - echo "" >&2 - echo "Available methods:" >&2 - echo " install - Installs an application" >&2 - echo " list - List all installed applications" >&2 - echo " list-versions - List all available versions for a single application" >&2 - echo " set-current - Set the current version" >&2 -} - -method_instance() { - local name="$1"; shift - local instance="$1"; shift - local method="$1" - - if [ $# -gt 0 ] - then - shift - fi - - case "$method" in - install) method_install "$name" "$instance" "$@" ;; - list) method_list "$name" "$instance" "$@" ;; - list-versions) method_list_versions "$name" "$instance" "$@" ;; - set-current) method_set_current "$name" "$instance" "$@" ;; - *) - if [ -z "$method" ] - then - method_instance_usage - else - method_instance_usage "Unknown method $method" - fi - ;; - esac - exit $? -} diff --git a/.app/lib/app-operate b/.app/lib/app-operate deleted file mode 100644 index 2b1408d..0000000 --- a/.app/lib/app-operate +++ /dev/null @@ -1,67 +0,0 @@ -#!/bin/bash - -operate_usage() { - if [ -n "$1" ] - then - echo "Error:" "$@" >&2 - fi - - echo "usage: $0 [operate method] -n name -i instance" >&2 - exit 1 -} - -method_operate_usage() { - if [ -n "$1" ] - then - echo "Error:" $@ >&2 - fi - - echo "usage: $0 operate " >&2 - echo "" >&2 - echo "Available operate methods:" >&2 - echo " start" >&2 - echo " stop" >&2 - echo " restart" >&2 - echo " status" >&2 -} - -method_operate() { - local name="$1"; shift - local instance="$1"; shift - local method="$1" - - if [ $# -gt 0 ] - then - shift - fi - - bin=`get_conf $apps $name $instance app.method` - - if [ -z "$bin" ] - then - bin=$APPSH_HOME/.app/lib/pid-method - fi - - if [ ! -x "$name/$instance/current/$bin" ] - then - echo "Invalid executable: $bin" >&2 - exit 1 - fi - - case "$method" in - start) run_app "$name" "$instance" "$bin" "start" "$@" ;; - stop) run_app "$name" "$instance" "$bin" "stop" "$@" ;; - status) run_app "$name" "$instance" "$bin" "status" "$@" ;; - restart) run_app "$name" "$instance" "$bin" "restart" "$@" ;; - run) run_app "$name" "$instance" "$bin" "run" "$@" ;; - *) - if [ -z "$method" ] - then - method_operate_usage - else - method_operate_usage "Unknown method $method" - fi - ;; - esac - exit $? -} diff --git a/.app/lib/pid-method b/.app/lib/pid-method deleted file mode 100755 index 29f6b4f..0000000 --- a/.app/lib/pid-method +++ /dev/null @@ -1,113 +0,0 @@ -#!/bin/bash -e - -set -u - -. $APPSH_HOME/.app/lib/app-conf - -pid_file=$APPSH_APPS/.app/var/pid/$APPSH_NAME-$APPSH_INSTANCE.pid -bin=`get_conf $APPSH_APPS $APPSH_NAME $APPSH_INSTANCE app.bin` - -cd $APPSH_APPS/$APPSH_NAME/$APPSH_INSTANCE/current - -if [ -z "$bin" ] -then - echo "Missing required configuration: app.bin." >&2 - exit 1 -fi - -if [ ! -r "$bin" ] -then - echo "No such file: $bin" >&2 - exit 1 -fi - -chmod +x "$bin" - -PID= -if [ -r $pid_file ] -then - PID="`cat $pid_file`" -fi - -do_status() { - if [ -z "$PID" ] - then - echo stopped - else - if [ `ps -p "$PID" 2>/dev/null | wc -l` -gt 1 ] - then - echo running - else - echo crashed - fi - fi -} - -method_start() { - case `do_status` in - running) - echo "The application is already running as $PID." - exit 1 - ;; - esac - - $bin <&- 1<&- 2<&- & - - PID=$! - echo "Application launched as $PID" - echo $PID > $pid_file - - return 0 -} - -method_stop() { - case `do_status` in - stopped) - echo "The application not running." - exit 1 - ;; - crashed) - echo "The application crashed. Was running as $PID" - # TODO: should this remove the PID file? - # That makes it possible to run "stop" to stop "status" from showing "crashed" - exit 1 - ;; - esac - - signal="-9" - echo -n "Sending kill $signal to $PID, waiting for shutdown" - kill $signal $PID - - while [ "`do_status`" == "running" ] - do - sleep 1 - echo -n "." - done - - echo " OK" - rm -f $pid_file - return 0 -} - -method_status() { - case `do_status` in - running) - echo "$APPSH_NAME/$APPSH_INSTANCE is running as $PID" - ;; - stopped) - echo "$APPSH_NAME/$APPSH_INSTANCE is not running" - ;; - crashed) - echo "$APPSH_NAME/$APPSH_INSTANCE crashed. Was running as $PID" - ;; - esac -} - -case "$APPSH_METHOD" in - start) method_start ;; - stop) method_stop ;; - status) method_status ;; - *) exit 1 ;; -esac - -exit $? diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2019e13 --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +all: test + +BATS=$(patsubst test/%,%,$(wildcard test/*.bats)) +TESTS=$(addprefix bats-,$(BATS)) + +bats-%: + @echo === test/$(patsubst bats-%,%,$@) + @bats test/$(patsubst bats-%,%,$@) + +test: $(TESTS) + @echo BATS=$(BATS) + @echo TESTS=$(TESTS) + +.PHONY: test diff --git a/NOTES.tmp b/NOTES.tmp new file mode 100644 index 0000000..1173e32 --- /dev/null +++ b/NOTES.tmp @@ -0,0 +1,16 @@ +app init maven io.trygvis.appsh.examples:foo:1.0-SNAPSHOT => + + # find resolver + app config set app.resolver maven + # run resolver/maven + # resolve version for artifact + # download artifact + app config set maven.id io.trygvis.appsh.examples:foo:1.0-SNAPSHOT + # run scripts/ + # create symlink + +app init [-d directory] + +resolver/maven -r + +app list - lists all applications available under the specified directory diff --git a/README.md b/README.md new file mode 100644 index 0000000..c4a83e9 --- /dev/null +++ b/README.md @@ -0,0 +1,166 @@ +Installation +------------ + + git clone http://.../app.sh.git + + mkdir /opt/apps + cd /opt/apps + ln -s .../app.sh.git/app.sh app.sh + +NOTE: The bash completion is not perfect yet. + + echo 'source .../app.sh.git/app_completion' >> ~/.bashrc + +Or was it `~/.bash_profile`? hmm + +TODOs +----- + +* Support installation-wide settings. Useful for shared environment + settings etc (PATH). + +* Add support for hooks in .app/hooks. Example hooks: + * Diff config. Save a backup of the config on installtaion. + * Copy the configuration from the previous installation. + +* Support changing current version. + +* Document app.sh + * Concept: config. group, key and value. + * Scriptable + +* init.d support - register the application under /etc/init.d on + installation. Should probably be a plugin. + +* Support -h for all applicable methods to show the help/usage. + +* Rename "scripts/" to handlers or something similar. Perhaps just + remove it entirely. + +* Document how the operate method (custom pid-method stuff) can see + its own output for debugging. + +Commands +-------- + +### `instance` + +#### `install` + +#### `upgrade` + +Tries to upgrade all instances where the version doesn't match the resolved version. + +#### `list` + +#### `list-versions` + +#### `set-current` + +#### `remove` + +Not implemented + +### `conf` + +#### `get` + + ./app -n $n -i $i conf get + +#### `set` + + ./app -n $n -i $i conf set group.key value + +#### `delete` + + ./app -n $n -i $i conf delete group.key + +### `operate` + +The operate sub-methods are provided by the application. + +#### Supported methods by `pid-method` + +#### `start` + +#### `stop` + +TODO: support -f flag that doesn't exit with 1 if the app is not running. + +#### `status` + +### `foreach` + +Runs the given command for each of the selected instances. + +Method Contract +--------------- + +### Environment variables you can depend on + +* `APPSH_NAME` +* `APPSH_INSTANCE` +* `APPSH_METHOD` + +Unclassified: + +* `APPSH_HOME` +* `APPSH_APPS` + +Directory Hierarchy +------------------- + +### Current + +App.sh is installed through cloning the git repository and/or +unpacking a tarball from the git repository. The directory that +contains the app.sh libraries is known as `$APPSH_HOME`. + +App.sh related: + + ./ The root of an application set. Known as $APPSH_APPS + ./app The app script, symlinked from your git clone directory + ./.app/lib bash libraries used by app.sh and methods + ./.app/var runtime data + +Applications: + + ./// - Known as $APPSH_INSTANCE_HOME + current -> - symlink to the currently installed app + versions/ - collection with all installed versions + 1.0/ - A installed version. The zip file is unzipped here. + root/ - The current directory when executing methods and scripts + scripts/ + 1.1/ + 2.0/ + +Creating Apps +------------- + +"App bundles" are basically just zip file with a specific layout. It +has to contain at least a `root/` directory which contains your +application. It can contain anything. The `current` symlink will point +to this directory. + +The bundle can also contain a special `scripts` directory which +contains scripts that's run durtion package operations. Only +`postinstall` is currently supported. + +Zip File Tree +============= + + root/ + bin/ + myapp + etc/ + config + scripts/ + postinstall + +Managing Apps +------------- + +Setting Environment Variables +----------------------------- + + app -n .. -i .. conf diff --git a/app b/app index a778e74..880026a 100755 --- a/app +++ b/app @@ -1,6 +1,4 @@ -#!/bin/bash - -set -e +#!/bin/bash -e PRG="$0" while [ -h "$PRG" ] ; do @@ -16,113 +14,56 @@ done APPSH_HOME=`dirname "$PRG"` APPSH_HOME=`cd "$APPSH_HOME" && pwd` -# Not sure this is useful -#if [ -z "$APPSH_APPS" ] -#then -# apps=`dirname $0` -# apps=`cd $apps; pwd` -#fi - -apps=`dirname $0` -apps=`cd $apps; pwd` - -# Ideally this should just do "cd /" to ensure that all paths are useful. -cd $apps - -mkdir -p $apps/.app/var/pid -mkdir -p $apps/.app/var/download - -method_usage() { +usage() { if [ -n "$1" ] then echo "Error:" "$@" >&2 fi - echo "usage: $0 [-n name] [-i instance] " >&2 + echo "usage: $0 " >&2 echo "" >&2 - echo "Available method groups:" >&2 - echo " instance" >&2 + echo "Available commands:" >&2 + echo " init" >&2 echo " conf" >&2 echo " operate" >&2 echo "" >&2 echo "Run $0 -h for more help" >&2 } -. $APPSH_HOME/.app/lib/app-common -. $APPSH_HOME/.app/lib/app-instance -. $APPSH_HOME/.app/lib/app-conf -. $APPSH_HOME/.app/lib/app-operate +. $APPSH_HOME/lib/common -main() { - local method - local name="$APPSH_NAME" - local instance="$APPSH_INSTANCE" +while getopts "h" opt +do + case $opt in + h) + usage + exit 1 + ;; + \?) + usage + exit 1 + ;; + esac +done - while getopts "n:i:h" opt - do - case $opt in - n) - name=$OPTARG - shift 2 - OPTIND=1 - ;; - i) - instance=$OPTARG - shift 2 - OPTIND=1 - ;; - h) - shift - OPTIND=1 - h="$1" +if [ $# -eq 0 ] +then + usage +fi - if [ -z "$h" ] - then - method_usage - else - case "$h" in - instance) - method_instance_usage - ;; - conf) - method_conf_usage - ;; - operate) - method_operate_usage - ;; - *) - echo "No such method group: $h" - ;; - esac - fi - exit 1 - ;; - \?) - echo "Invalid option: $OPTARG" - ;; - esac - done +command=$1; shift - local method=$1 - if [ $# -gt 0 ] - then - shift - fi +bin=`grep_path "/app-$command$" "$APPSH_HOME/bin"` - case "$method" in - instance) method_instance "$name" "$instance" "$@" ;; - conf) method_conf "$name" "$instance" "$@" ;; - operate) method_operate "$name" "$instance" "$@" ;; - *) - if [ -z "$method" ] - then - method_usage - else - method_usage "No such method group: $method" - fi - ;; - esac - exit $? -} +if [ ! -x "$bin" ] +then + echo "Unknown command: $command" 2>&1 + exit 1 +fi + +PATH=$APPSH_HOME/bin:$PATH -main "$@" +# TODO: this is probably a good place to clean up the environment +exec env \ + "APPSH_HOME=$APPSH_HOME" \ + "$bin" "$@" diff --git a/bin/app-conf b/bin/app-conf new file mode 100755 index 0000000..7193c20 --- /dev/null +++ b/bin/app-conf @@ -0,0 +1,135 @@ +#!/bin/bash + +# TODO: Add a 'get' command that returns a single value +# Exit with 0 if found, 1 otherwise. + +if [[ $APPSH_HOME == "" ]] +then + APPSH_HOME=`dirname "$0"` + APPSH_HOME=`cd "$APPSH_HOME/.." && pwd` +fi + +. $APPSH_HOME/lib/common + +PATH=$APPSH_HOME/libexec:$PATH + +key_expr="[a-zA-Z][_a-zA-Z0-9]*" + +format_conf() { + local IFS== + while read key value + do + printf "%-20s %-20s" "$key" "$value" + echo + done +} + +assert_valid_config_name() { + local name=$1 + + local x=`echo $name | sed -n "/^$key_expr\\.$key_expr$/p"` + if [ -z "$x" ] + then + echo "Invalid name: $name" >&2 + exit 1 + fi +} + +conf_set() { + local name=$1; shift + local value=$1; shift + + assert_valid_config_name "$name" + + if [ -r $file ] + then + sed "/^$name[ ]*=.*/d" $file > $file.tmp + fi + + echo "$name=$value" >> $file.tmp + mv $file.tmp $file +} + +conf_delete() { + local name=$1; shift + + assert_valid_config_name "$name" + + sed "/^$name[ ]*=.*/d" $file > $file.tmp + mv $file.tmp $file +} + +usage() { + if [ -n "$1" ] + then + echo "Error: $@" >&2 + fi + + echo "usage: $0 conf " >&2 + echo "" + echo "Available commands:" >&2 + echo " list - list all config values" >&2 + echo " set [name] [value] - set a config parameter" >&2 + echo " delete [name] - deletes a config parameter" >&2 + exit 1 +} + +if [ $# -gt 0 ] +then + command=$1 + shift +else + command=list +fi + +file=".app/config" + +assert_is_app -C + +case "$command" in + get) + if [ $# != 1 ] + then + usage + exit 1 + fi + + app-cat-conf -f "$file" -n "$1" | cut -f 2 -d = | format_conf | sed "s, *$,," + ;; + list) + if [ $# -gt 0 ] + then + usage "Extra options." + exit 1 + fi + + app-cat-conf -f "$file" | format_conf + ;; + set) + if [ $# -ne 2 ] + then + usage + exit 1 + fi + + conf_set "$1" "$2" + ;; + delete) + if [ $# -ne 1 ] + then + usage "Missing [name] argument." + exit 1 + fi + + conf_delete "$1" "$2" + ;; + *) + if [ -z "$command" ] + then + usage + else + usage "Unknown command: $command" + fi + exit 1 + ;; +esac diff --git a/bin/app-init b/bin/app-init new file mode 100755 index 0000000..e758916 --- /dev/null +++ b/bin/app-init @@ -0,0 +1,79 @@ +#!/bin/bash -e + +set -u + +if [[ $APPSH_HOME == "" ]] +then + APPSH_HOME=`dirname "$0"` + APPSH_HOME=`cd "$APPSH_HOME/.." && pwd` +fi + +. $APPSH_HOME/lib/common + +usage() { + echo "usage: $0 -d dir " + exit 1 +} + +fatal() { + echo "$0: $@" + exit 1 +} + +while getopts "d:" opt +do + case $opt in + d) + dir=$OPTARG + shift 2 + OPTIND=1 + ;; + esac +done + +if [ $# -lt 1 ] +then + usage +fi + +resolver_name="$1"; shift + +if [ -z "$dir" ] +then + usage +fi + +if [ -e "$dir" ] +then + fatal "Already initialized: $dir" 2>&1 +fi + +# TODO: install a trap handler and rm -rf "$dir" + +resolver=`grep_path "/app-resolver-$resolver_name$" "$PATH:$APPSH_HOME/libexec" | head -n 1` + +if [ -z "$resolver" ] +then + echo "No such resolver: $resolver_name" 2>&1 + exit 1 +fi + +mkdir -- "$dir" "$dir/.app" +cd "$dir" + +app-conf set app.resolver "$resolver_name" + +"$resolver" init "$@" +"$resolver" resolve-version + +version=`app-conf get app.version` + +if [[ $version == "" ]] +then + echo "Unable to resolve version" 2>&1 + exit +fi + +echo "Resolved version to $version" + +"$resolver" download-version -v "$version" -f .app/latest.zip diff --git a/bin/app-instance b/bin/app-instance new file mode 100755 index 0000000..02e3c0f --- /dev/null +++ b/bin/app-instance @@ -0,0 +1,502 @@ +#!/bin/bash + +if [[ $APPSH_HOME == "" ]] +then + APPSH_HOME=`dirname "$0"` + APPSH_HOME=`cd "$APPSH_HOME/.." && pwd` +fi + +if [ -n "$APPSH_REPO" ] +then + repo="$APPSH_REPO" +else + repo="http://repo1.maven.org" +fi + +calculate_md5() { + local file="$1"; shift + + md5sum "$file" | cut -c 1-32 +} + +# TODO: support file:// repositories +# TODO: look in the local repository first +get() { + local url=$1 + local file=$2 + local exit + + curl -o $file $url -D curl.tmp + + exit=`grep "^HTTP/[0-9]\.[0-9] 200 .*" curl.tmp >/dev/null; echo $?` + head=`head -n 1 curl.tmp` + rm -f curl.tmp + if [ "$exit" != 0 ] + then + echo "Unable to download $url: $head" >&2 + exit 1 + fi +} + +resolve_snapshot() { + local groupId=$1; shift + local groupIdSlash=$1; shift + local artifactId=$1; shift + local version=$1; shift + + local metadata=$apps/.app/var/download/$groupId-$artifactId-$version-metadata.xml + local base_url=$repo/$groupIdSlash/$artifactId/$version + get $base_url/maven-metadata.xml $metadata + local resolved_version=`xmlstarlet sel -t -m '//snapshotVersion[extension[text()="zip"]]' -v value $metadata` + echo $resolved_version +} + +download_artifact() { + local file="$1"; shift + local url="$1"; shift + + echo "Downloading $url.md5" + get $url.md5 $file.md5 + local expected_md5="`cat $file.md5`" + + if [ -r $file ] + then + if [ "$expected_md5" == "`calculate_md5 $file`" ] + then + echo "Artifact already downloaded." + else + rm -f "$file" + fi + return 0 + fi + echo "Downloading artifact: $url" + get $url $file + + local actual_md5="`calculate_md5 $file`" + if [ "$expected_md5" == "$actual_md5" ] + then + echo "Artifact downloaded." + else + echo "Invalid checksum. Expected $expected_md5, got $actual_md5" >&2 + exit 1 + fi +} + +method_install_usage() { + if [ -n "$1" ] + then + echo "Error:" "$@" >&2 + fi + + echo "usage: install <-r resolver> -u " >&2 + echo "" >&2 + echo "Install package from a Maven repository:" >&2 + echo " $0 [-n name] [-i instance] instance install -r maven -u groupId:artifactId:version" >&2 + echo "" >&2 + echo "Install package from a file:" >&2 + echo " $0 [-n name] [-i instance] instance install -r file -u file [-v version]" >&2 + echo "The version defaults to the current timestamp" >&2 + exit 1 +} + +method_install() { + local name="$1"; shift + local instance="$1"; shift + local version + local resolver + local url + local groupId + local artifactId + local zip_file + + + if [ $# -eq 0 ] + then + method_install_usage + fi + + while getopts "n:i:v:r:u:" opt + do + case $opt in + n) + name=$OPTARG + ;; + i) + instance=$OPTARG + ;; + v) + version=$OPTARG + ;; + r) + resolver=$OPTARG + ;; + u) + url=$OPTARG + ;; + \?) + method_install_usage "Invalid option: -$OPTARG" + ;; + esac + done + + if [ -z "$name" ] + then + method_install_usage "Missing required argument: -i name." + fi + + if [ -z "$instance" ] + then + method_install_usage "Missing required argument: -i instance." + fi + + if [ -z "$resolver" ] + then + method_install_usage "Missing required option: -r resolver" + fi + + if [ -z "$url" ] + then + method_install_usage "Missing required option: -u url" + fi + + case "$resolver" in + maven) + url=`echo $url | tr ":" " "`; set -- $url + groupId=$1 + artifactId=$2 + version=$3 + + if [ -z "$groupId" -o -z "$artifactId" -o -z "$version" ] + then + method_install_usage "Invalid Maven url." + fi + + local groupIdSlash=$(echo $groupId | sed "s,\.,/,g") + if [ "`echo $version | sed -n s,.*-SNAPSHOT$,SNAPSHOT,p`" == "SNAPSHOT" ] + then + echo "Resolving version $version..." + local resolved_version=`resolve_snapshot $groupId $groupIdSlash $artifactId $version` + if [ -z "$resolved_version" ] + then + echo "Unable to resolve version." + exit 1 + fi + echo "Resolved version $version to $resolved_version" + else + resolved_version=$version + fi + + zip_file=$apps/.app/var/download/$groupId-$artifactId-$resolved_version.zip + artifact_url=$repo/$groupIdSlash/$artifactId/$version/$artifactId-$resolved_version.zip + + download_artifact "$zip_file" "$artifact_url" + ;; + file) + if [ ! -r "$url" ] + then + echo "Could not read file: $url" >&2 + exit 1 + fi + + # TODO: should the zip file be copied into download/ so that + # there's always a local copy? + zip_file=$url + + if [ -z "$version" ] + then + version=`TZ=UTC date +"%Y%m%d-%H%M%S"` + fi + + resolved_version=$version + ;; + *) + method_install_usage "Invalid resolver type: $resolver" + ;; + esac + + if [ -d $name/$instance/versions/$resolved_version ] + then + echo "Version $resolved_version is already installed" + exit 1 + fi + + if [ ! -d $name/$instance ] + then + echo "Creating instance '$instance' for '$name'" + mkdir -p $name/$instance + fi + + mkdir -p $name/$instance/versions/$resolved_version + + echo "Unpacking..." + unzip -q -d $name/$instance/versions/$resolved_version $zip_file + + if [ ! -d $name/$instance/versions/$resolved_version/root ] + then + echo "Invalid zip file, did not contain a ./root directory." >&2 + exit 1 + fi + + echo "Changing current symlink" + rm -f $apps/$name/$instance/current + ln -s versions/$resolved_version/root $apps/$name/$instance/current + + if [ -d $name/$instance/current/bin ] + then + ( + cd $name/$instance/current + find bin -type f | xargs chmod +x + ) + fi + + ( + cd $name/$instance/versions/$resolved_version + if [ -d scripts ] + then + find scripts | xargs chmod +x + fi + + if [ -x scripts/postinstall ] + then + echo "Running postinstall..." + cd root + set +e + env -i \ + PATH=/bin:/usr/bin \ + APPSH_APPS=$apps \ + APPSH_HOME=$APPSH_HOME \ + APPSH_NAME=$name \ + APPSH_INSTANCE=$instance \ + APPSH_VERSION=$resolved_version \ + ../scripts/postinstall + set -e + ret=`echo $?` + if [ "$ret" != 0 ] + then + echo "Postinstall failed!" + exit 1 + fi + echo "Postinstall completed successfully" + fi + ) + + if [ -r $apps/.app/var/list ] + then + sed "/^$name:$instance/d" $apps/.app/var/list > $apps/.app/var/list.new + fi + echo "$name:$instance:$version:$url" >> $apps/.app/var/list.new + mv $apps/.app/var/list.new $apps/.app/var/list +} + +method_set_current_usage() { + if [ -n "$1" ] + then + echo "Error:" "$@" >&2 + fi + + echo "usage: set-current -v version" >&2 + exit 1 +} + +method_set_current() { + local name="$1"; shift + local instance="$1"; shift + local version + + if [ $# -eq 0 ] + then + method_set_current_usage + fi + + while getopts "n:i:v:" opt + do + case $opt in + n) + name=$OPTARG + ;; + i) + instance=$OPTARG + ;; + v) + version=$OPTARG + ;; + \?) + method_set_current_usage "Invalid option: -$OPTARG" + ;; + esac + done + + if [ -z "$version" ] + then + echo "Missing required option -v version." >&2 + exit 1 + fi + + assert_is_instance method_set_current_usage "$name" "$instance" "no" + + if [ ! -d $apps/$name/$instance/versions/$version ] + then + echo "Invalid version: $version." + exit 1 + fi + + rm -f $apps/$name/$instance/current + ln -s versions/$version/root $apps/$name/$instance/current + + return 0 +} + +method_list_usage() { + if [ -n "$1" ] + then + echo "Error:" "$@" >&2 + fi + + echo "usage: list [-n name] [-P field]" >&2 + echo "" + echo "List all installed applications:" >&2 + echo " list" >&2 + echo "" + echo "List all applications with the selected fields with parseable output:" >&2 + echo " list -P instance -P version -n foo" >&2 + exit 1 +} + +method_list() { + local filter_name="$1"; shift + local filter_instance="$1"; shift + local mode="pretty" + local vars + local filter_name + + while getopts "P:n:i:" opt + do + case $opt in + P) + mode="parseable" + vars="$vars $OPTARG" + ;; + n) + filter_name=$OPTARG + ;; + i) + filter_instance=$OPTARG + ;; + \?) + method_list_usage "Invalid option: -$OPTARG" + ;; + esac + done + + if [ ! -r $apps/.app/var/list ] + then + return + fi + + if [ $mode = "pretty" ] + then + printf "%-20s %-20s %-20s\n" "Name" "Instance" "Version" + list_apps "$filter_name" "$filter_instance" name instance version | (IFS=:; while read name instance version + do + printf "%-20s %-20s %-20s\n" "$name" "$instance" "$version" + done) + else + list_apps "$filter_name" "$filter_instance" $vars + fi +} + +method_list_versions_usage() { + if [ -n "$1" ] + then + echo "Error:" "$@" >&2 + fi + + echo "usage: list-versions -n name -i instance [-P]" >&2 + exit 1 +} + +method_list_versions() { + local filter_name="$1"; shift + local instance="$1"; shift + local version + local mode="pretty" + + if [ $# -eq 0 ] + then + method_list_versions_usage + fi + + while getopts "n:i:P" opt + do + case $opt in + n) + name=$OPTARG + ;; + i) + instance=$OPTARG + ;; + v) + version=$OPTARG + ;; + P) + mode="parseable" + ;; + \?) + method_list_versions_usage "Invalid option: -$OPTARG" + ;; + esac + done + + assert_is_instance method_list_versions_usage "$name" "$instance" "no" + + if [ $mode = "pretty" ] + then + echo "Available versions for $name/$instance:" + fi + + find_versions $name $instance + + return 0 +} + +method_instance_usage() { + if [ -n "$1" ] + then + echo "Error:" $@ >&2 + fi + + echo "usage: $0 instance " >&2 + echo "" >&2 + echo "Available methods:" >&2 + echo " install - Installs an application" >&2 + echo " list - List all installed applications" >&2 + echo " list-versions - List all available versions for a single application" >&2 + echo " set-current - Set the current version" >&2 +} + +method_instance() { + local name="$1"; shift + local instance="$1"; shift + local method="$1" + + if [ $# -gt 0 ] + then + shift + fi + + case "$method" in + install) method_install "$name" "$instance" "$@" ;; + list) method_list "$name" "$instance" "$@" ;; + list-versions) method_list_versions "$name" "$instance" "$@" ;; + set-current) method_set_current "$name" "$instance" "$@" ;; + *) + if [ -z "$method" ] + then + method_instance_usage + else + method_instance_usage "Unknown method $method" + fi + ;; + esac + exit $? +} diff --git a/bin/app-operate b/bin/app-operate new file mode 100755 index 0000000..dc16780 --- /dev/null +++ b/bin/app-operate @@ -0,0 +1,73 @@ +#!/bin/bash + +if [[ $APPSH_HOME == "" ]] +then + APPSH_HOME=`dirname "$0"` + APPSH_HOME=`cd "$APPSH_HOME/.." && pwd` +fi + +operate_usage() { + if [ -n "$1" ] + then + echo "Error:" "$@" >&2 + fi + + echo "usage: $0 [operate method] -n name -i instance" >&2 + exit 1 +} + +method_operate_usage() { + if [ -n "$1" ] + then + echo "Error:" $@ >&2 + fi + + echo "usage: $0 operate " >&2 + echo "" >&2 + echo "Available operate methods:" >&2 + echo " start" >&2 + echo " stop" >&2 + echo " restart" >&2 + echo " status" >&2 +} + +method_operate() { + local name="$1"; shift + local instance="$1"; shift + local method="$1" + + if [ $# -gt 0 ] + then + shift + fi + + bin=`$APPSH_HOME/bin/app-cat-conf -f $apps/$name/$instance/current/etc/app.conf -g app -k method | cut -f 2 -d =` + + if [ -z "$bin" ] + then + bin=$APPSH_HOME/.app/lib/pid-method + fi + + if [ ! -x "$name/$instance/current/$bin" ] + then + echo "Invalid executable: $bin" >&2 + exit 1 + fi + + case "$method" in + start) run_app "$name" "$instance" "$bin" "start" "$@" ;; + stop) run_app "$name" "$instance" "$bin" "stop" "$@" ;; + status) run_app "$name" "$instance" "$bin" "status" "$@" ;; + restart) run_app "$name" "$instance" "$bin" "restart" "$@" ;; + run) run_app "$name" "$instance" "$bin" "run" "$@" ;; + *) + if [ -z "$method" ] + then + method_operate_usage + else + method_operate_usage "Unknown method $method" + fi + ;; + esac + exit $? +} diff --git a/bin/pid-method b/bin/pid-method new file mode 100755 index 0000000..29f6b4f --- /dev/null +++ b/bin/pid-method @@ -0,0 +1,113 @@ +#!/bin/bash -e + +set -u + +. $APPSH_HOME/.app/lib/app-conf + +pid_file=$APPSH_APPS/.app/var/pid/$APPSH_NAME-$APPSH_INSTANCE.pid +bin=`get_conf $APPSH_APPS $APPSH_NAME $APPSH_INSTANCE app.bin` + +cd $APPSH_APPS/$APPSH_NAME/$APPSH_INSTANCE/current + +if [ -z "$bin" ] +then + echo "Missing required configuration: app.bin." >&2 + exit 1 +fi + +if [ ! -r "$bin" ] +then + echo "No such file: $bin" >&2 + exit 1 +fi + +chmod +x "$bin" + +PID= +if [ -r $pid_file ] +then + PID="`cat $pid_file`" +fi + +do_status() { + if [ -z "$PID" ] + then + echo stopped + else + if [ `ps -p "$PID" 2>/dev/null | wc -l` -gt 1 ] + then + echo running + else + echo crashed + fi + fi +} + +method_start() { + case `do_status` in + running) + echo "The application is already running as $PID." + exit 1 + ;; + esac + + $bin <&- 1<&- 2<&- & + + PID=$! + echo "Application launched as $PID" + echo $PID > $pid_file + + return 0 +} + +method_stop() { + case `do_status` in + stopped) + echo "The application not running." + exit 1 + ;; + crashed) + echo "The application crashed. Was running as $PID" + # TODO: should this remove the PID file? + # That makes it possible to run "stop" to stop "status" from showing "crashed" + exit 1 + ;; + esac + + signal="-9" + echo -n "Sending kill $signal to $PID, waiting for shutdown" + kill $signal $PID + + while [ "`do_status`" == "running" ] + do + sleep 1 + echo -n "." + done + + echo " OK" + rm -f $pid_file + return 0 +} + +method_status() { + case `do_status` in + running) + echo "$APPSH_NAME/$APPSH_INSTANCE is running as $PID" + ;; + stopped) + echo "$APPSH_NAME/$APPSH_INSTANCE is not running" + ;; + crashed) + echo "$APPSH_NAME/$APPSH_INSTANCE crashed. Was running as $PID" + ;; + esac +} + +case "$APPSH_METHOD" in + start) method_start ;; + stop) method_stop ;; + status) method_status ;; + *) exit 1 ;; +esac + +exit $? diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index c4a83e9..0000000 --- a/docs/README.md +++ /dev/null @@ -1,166 +0,0 @@ -Installation ------------- - - git clone http://.../app.sh.git - - mkdir /opt/apps - cd /opt/apps - ln -s .../app.sh.git/app.sh app.sh - -NOTE: The bash completion is not perfect yet. - - echo 'source .../app.sh.git/app_completion' >> ~/.bashrc - -Or was it `~/.bash_profile`? hmm - -TODOs ------ - -* Support installation-wide settings. Useful for shared environment - settings etc (PATH). - -* Add support for hooks in .app/hooks. Example hooks: - * Diff config. Save a backup of the config on installtaion. - * Copy the configuration from the previous installation. - -* Support changing current version. - -* Document app.sh - * Concept: config. group, key and value. - * Scriptable - -* init.d support - register the application under /etc/init.d on - installation. Should probably be a plugin. - -* Support -h for all applicable methods to show the help/usage. - -* Rename "scripts/" to handlers or something similar. Perhaps just - remove it entirely. - -* Document how the operate method (custom pid-method stuff) can see - its own output for debugging. - -Commands --------- - -### `instance` - -#### `install` - -#### `upgrade` - -Tries to upgrade all instances where the version doesn't match the resolved version. - -#### `list` - -#### `list-versions` - -#### `set-current` - -#### `remove` - -Not implemented - -### `conf` - -#### `get` - - ./app -n $n -i $i conf get - -#### `set` - - ./app -n $n -i $i conf set group.key value - -#### `delete` - - ./app -n $n -i $i conf delete group.key - -### `operate` - -The operate sub-methods are provided by the application. - -#### Supported methods by `pid-method` - -#### `start` - -#### `stop` - -TODO: support -f flag that doesn't exit with 1 if the app is not running. - -#### `status` - -### `foreach` - -Runs the given command for each of the selected instances. - -Method Contract ---------------- - -### Environment variables you can depend on - -* `APPSH_NAME` -* `APPSH_INSTANCE` -* `APPSH_METHOD` - -Unclassified: - -* `APPSH_HOME` -* `APPSH_APPS` - -Directory Hierarchy -------------------- - -### Current - -App.sh is installed through cloning the git repository and/or -unpacking a tarball from the git repository. The directory that -contains the app.sh libraries is known as `$APPSH_HOME`. - -App.sh related: - - ./ The root of an application set. Known as $APPSH_APPS - ./app The app script, symlinked from your git clone directory - ./.app/lib bash libraries used by app.sh and methods - ./.app/var runtime data - -Applications: - - ./// - Known as $APPSH_INSTANCE_HOME - current -> - symlink to the currently installed app - versions/ - collection with all installed versions - 1.0/ - A installed version. The zip file is unzipped here. - root/ - The current directory when executing methods and scripts - scripts/ - 1.1/ - 2.0/ - -Creating Apps -------------- - -"App bundles" are basically just zip file with a specific layout. It -has to contain at least a `root/` directory which contains your -application. It can contain anything. The `current` symlink will point -to this directory. - -The bundle can also contain a special `scripts` directory which -contains scripts that's run durtion package operations. Only -`postinstall` is currently supported. - -Zip File Tree -============= - - root/ - bin/ - myapp - etc/ - config - scripts/ - postinstall - -Managing Apps -------------- - -Setting Environment Variables ------------------------------ - - app -n .. -i .. conf diff --git a/lib/common b/lib/common new file mode 100644 index 0000000..22c8cd0 --- /dev/null +++ b/lib/common @@ -0,0 +1,168 @@ +#!/bin/bash + +assert_is_app() { + local check_link=yes + + while getopts "C" opt + do + case $opt in + C) + check_link=no + ;; + esac + done + + if [ ! -d .app ] + then + echo "This is not an app, missing directory: '.app'" >&2 + exit 1 + fi + + if [[ $check_link == yes ]] + then + if [ ! -e current ] + then + echo "Missing 'current' link." >&2 + exit 1 + fi + fi +} + +list_apps() { + local filter_name=$1; shift + local filter_instnace=$1; shift + local vars="$@" + + sort $apps/.app/var/list | while read line + do + echo $line | (IFS=:; while read name instance version junk + do + if [ -n "$filter_name" -a "$filter_name" != "$name" ] + then + continue + fi + + if [ -n "$filter_instance" -a "$filter_instance" != "$instance" ] + then + continue + fi + + local line="" + IFS=" "; for var in $vars + do + case $var in + name) x=$name;; + instance) x=$instance;; + version) x=$version;; + current_version) x=`find_current_version $name $instance`;; + *) x="";; + esac + + if [ -z "$line" ] + then + line="$line$x" + else + line="$line:$x" + fi + done + echo $line + done) + done +} + +find_current_version() { + local name=$1 + local instance=$2 + + if [ ! -L $apps/$name/$instance/current ] + then + return 0 + fi + + ( + cd $apps/$name/$instance + ls -l current | sed -n "s,.* current -> versions/\(.*\)/root,\1,p" + ) +} + +find_versions() { + local name="$1" + local instance="$2" + + if [ ! -d $apps/$name/$instance/versions ] + then + return 0 + fi + + ( + cd $apps/$name/$instance/versions + ls -1d * + ) +} + +grep_path() { + local regex="$1"; shift + local path="$1"; shift + + find `echo $path | tr : " "` -type f -executable 2>/dev/null | (egrep "$regex" || exit 0) +# IFS=: +# for x in $path +# do +# ls $x/* 2>/dev/null | while read f +# do +# if [[ $f =~ $regex ]] +# then +# echo $f +# fi +# done +# done +} + +# TODO: set ulimit +# TODO: set umask +# TODO: change group newgrp/sg +run_app() { + local name=$1; shift + local instance=$1; shift + local bin=$1; shift + local method=$1; shift + + assert_is_instance operate_usage "$name" "$instance" + + local x="" + if [ ! -z "$APPSH_APPS" ] + then + x="$APPSH_APPS/" + fi + + ( + cd $x$name/$instance + APPSH_INSTANCE_HOME=`pwd` + cd current + + local e=`$APPSH_HOME/bin/app-cat-conf -f $apps/$name/$instance/current/etc/app.conf -g env | cut -f 2- -d .` + #e="`get_conf_in_group $apps $name $instance env`" + # This magically get the expansion of $u correct. + IFS=" +" + + # Set a default PATH which can be overridden by the application's settings + set +e + env -i \ + PATH=/bin:/usr/bin \ + $e \ + PWD="$PWD" \ + APPSH_METHOD=$method \ + APPSH_APPS=$apps \ + APPSH_HOME=$APPSH_HOME \ + APPSH_NAME=$name \ + APPSH_INSTANCE=$instance \ + APPSH_INSTANCE_HOME=$APPSH_INSTANCE_HOME \ + $bin "$@" + local ret=$? + set +x + set -e + + exit $ret + ) +} diff --git a/lib/default-config b/lib/default-config new file mode 100644 index 0000000..855cc8b --- /dev/null +++ b/lib/default-config @@ -0,0 +1 @@ +maven.repo=http://repo1.maven.org diff --git a/libexec/app-cat-conf b/libexec/app-cat-conf new file mode 100755 index 0000000..78d3c36 --- /dev/null +++ b/libexec/app-cat-conf @@ -0,0 +1,47 @@ +#!/bin/bash + +if [[ $APPSH_HOME == "" ]] +then + APPSH_HOME=`dirname "$0"` + APPSH_HOME=`cd "$APPSH_HOME/.." && pwd` +fi + +set -e + +key_expr="[a-zA-Z][_a-zA-Z0-9]*" + +file=.app/config + +while getopts "f:n:" opt +do + case $opt in + f) + file=$OPTARG + ;; + n) + name=$OPTARG + ;; + \?) + echo "Invalid option: $OPTARG" >&2 + exit 1 + ;; + esac +done + +if [ -z "$name" ] +then + filter="s,^[ ]*\($key_expr\.$key_expr\)[ ]*=[ ]*\(.*\)$,\1=\2,p" +else + filter="s,^\($name\)=\(.*\),\1=\2,p" +fi + +if [[ $APPSH_DEFAULT_CONFIG == "" ]] +then + APPSH_DEFAULT_CONFIG=$APPSH_HOME/lib/default-config +fi + +# The awk script makes sure each key only appears once +cat "$file" "$APPSH_DEFAULT_CONFIG" | \ + sed -n -e "$filter" $extra | \ + awk -F = ' (!($1 in a)){a[$1]; print }' | \ + sort diff --git a/libexec/app-grep-path b/libexec/app-grep-path new file mode 100755 index 0000000..f5e8287 --- /dev/null +++ b/libexec/app-grep-path @@ -0,0 +1,13 @@ +#!/bin/bash + +# A command line wrapper around get grep_path function + +if [[ $APPSH_HOME == "" ]] +then + APPSH_HOME=`dirname "$0"` + APPSH_HOME=`cd "$APPSH_HOME/.." && pwd` +fi + +. $APPSH_HOME/lib/common + +grep_path "$1" "$2" diff --git a/libexec/app-install-file b/libexec/app-install-file new file mode 100755 index 0000000..6c06aa7 --- /dev/null +++ b/libexec/app-install-file @@ -0,0 +1,172 @@ +#!/bin/bash + +if [[ $APPSH_HOME == "" ]] +then + APPSH_HOME=`dirname "$0"` + APPSH_HOME=`cd "$APPSH_HOME/.." && pwd` +fi + +calculate_md5() { + local file="$1"; shift + + md5sum "$file" | cut -c 1-32 +} + +# TODO: support file:// repositories +# TODO: look in the local repository first +get() { + local url=$1 + local file=$2 + local exit + + curl -o $file $url -D curl.tmp + + exit=`grep "^HTTP/[0-9]\.[0-9] 200 .*" curl.tmp >/dev/null; echo $?` + head=`head -n 1 curl.tmp` + rm -f curl.tmp + if [ "$exit" != 0 ] + then + echo "Unable to download $url: $head" >&2 + exit 1 + fi +} + +resolve_snapshot() { + local groupId=$1; shift + local groupIdSlash=$1; shift + local artifactId=$1; shift + local version=$1; shift + + local metadata=$apps/.app/var/download/$groupId-$artifactId-$version-metadata.xml + local base_url=$repo/$groupIdSlash/$artifactId/$version + get $base_url/maven-metadata.xml $metadata + local resolved_version=`xmlstarlet sel -t -m '//snapshotVersion[extension[text()="zip"]]' -v value $metadata` + echo $resolved_version +} + +download_artifact() { + local file="$1"; shift + local url="$1"; shift + + echo "Downloading $url.md5" + get $url.md5 $file.md5 + local expected_md5="`cat $file.md5`" + + if [ -r $file ] + then + if [ "$expected_md5" == "`calculate_md5 $file`" ] + then + echo "Artifact already downloaded." + else + rm -f "$file" + fi + return 0 + fi + echo "Downloading artifact: $url" + get $url $file + + local actual_md5="`calculate_md5 $file`" + if [ "$expected_md5" == "$actual_md5" ] + then + echo "Artifact downloaded." + else + echo "Invalid checksum. Expected $expected_md5, got $actual_md5" >&2 + exit 1 + fi +} + +if [ $# -lt 2 ] +then + method_install_usage +fi + +case "$resolver" in + maven) + ;; + file) + if [ ! -r "$url" ] + then + echo "Could not read file: $url" >&2 + exit 1 + fi + + # TODO: should the zip file be copied into download/ so that + # there's always a local copy? + zip_file=$url + + if [ -z "$version" ] + then + version=`TZ=UTC date +"%Y%m%d-%H%M%S"` + fi + + resolved_version=$version + ;; + *) + method_install_usage "Invalid resolver type: $resolver" + ;; +esac + +if [ -d versions/$resolved_version ] +then + echo "Version $resolved_version is already installed" + exit 1 +fi + +mkdir -p versions/$resolved_version + +echo "Unpacking..." +unzip -q -d versions/$resolved_version $zip_file + +if [ ! -d versions/$resolved_version/root ] +then + echo "Invalid zip file, did not contain a ./root directory." >&2 + exit 1 +fi + +echo "Changing current symlink" +rm -f current +ln -s versions/$resolved_version/root current + +if [ -d current/bin ] +then + ( + cd $name/$instance/current + find bin -type f | xargs chmod +x + ) +fi + +( + cd versions/$resolved_version + if [ -d scripts ] + then + find scripts | xargs chmod +x + fi + + if [ -x scripts/postinstall ] + then + echo "Running postinstall..." + cd root + set +e + env -i \ + PATH=/bin:/usr/bin \ + APPSH_APPS=$apps \ + APPSH_HOME=$APPSH_HOME \ + APPSH_VERSION=$resolved_version \ + ../scripts/postinstall + set -e + ret=`echo $?` + if [ "$ret" != 0 ] + then + echo "Postinstall failed!" + exit 1 + fi + echo "Postinstall completed successfully" + fi +) + +# if [ -r $apps/.app/var/list ] +# then +# sed "/^$name:$instance/d" $apps/.app/var/list > $apps/.app/var/list.new +# fi +# echo "$name:$instance:$version:$url" >> $apps/.app/var/list.new +# mv $apps/.app/var/list.new $apps/.app/var/list diff --git a/libexec/app-resolver-maven b/libexec/app-resolver-maven new file mode 100755 index 0000000..cb137fa --- /dev/null +++ b/libexec/app-resolver-maven @@ -0,0 +1,231 @@ +#!/bin/bash -e + +set -u + +if [[ $APPSH_HOME == "" ]] +then + APPSH_HOME=`dirname "$0"` + APPSH_HOME=`cd "$APPSH_HOME/.." && pwd` +fi + +usage() { + message=${1-} + + if [ ! -z "$message" ] + then + echo $message + fi + + echo "usage: $subapp init -r " + echo "usage: $subapp resolve-version" + echo "usage: $subapp download-version -v -f " + exit 1 +} + +slash() { + echo $1 | sed "s,\.,/,g" +} + +# TODO: support file:// repositories +# TODO: look in the local repository first +get() { + local url=$1; shift + local file=$1; shift + + if [[ $url == file://* ]] + then + get_file "$url" "$file" + else + get_http "$url" "$file" + fi +} + +get_file() { + url=${1:7} + + cp "$url" "$2" +} + +get_http() { + curl -o $file $url -D curl.tmp + + exit=`grep "^HTTP/[0-9]\.[0-9] 200 .*" curl.tmp >/dev/null; echo $?` + head=`head -n 1 curl.tmp` + rm -f curl.tmp + if [ "$exit" != 0 ] + then + echo "Unable to download $url: $head" >&2 + exit 1 + fi +} + +download_artifact() { + local file="$1"; shift + local url="$1"; shift + + echo "Downloading $url.md5" + get $url.md5 $file.md5 + local expected_md5="`cat $file.md5`" + + if [ -r $file ] + then + if [ "$expected_md5" == "`calculate_md5 $file`" ] + then + echo "Artifact already downloaded." + else + rm -f "$file" + fi + return 0 + fi + echo "Downloading artifact: $url" + get $url $file + + local actual_md5="`calculate_md5 $file`" + if [ "$expected_md5" == "$actual_md5" ] + then + echo "Artifact downloaded." + else + echo "Invalid checksum. Expected $expected_md5, got $actual_md5" >&2 + exit 1 + fi +} + +resolve_version() { + local group_id=`app-conf get maven.group_id` + local artifact_id=`app-conf get maven.artifact_id` + local version=`app-conf get maven.version` + + repo=`app-conf get maven.repo` + + if [[ ! $version == *-SNAPSHOT ]] + then + app-conf set app.version "$version" + exit 0 + fi + + echo "Resolving version $version..." + resolve_snapshot $group_id $artifact_id $version +} + +resolve_snapshot() { + local group_id=$1; shift + local artifact_id=$1; shift + local version=$1; shift + + repo=`app-conf get maven.repo` + + local group_id_slash=`slash $group_id` + + local base_path=$group_id_slash/$artifact_id/$version + + mkdir -p .app/cache/$base_path + + local l=.app/cache/$base_path/maven-metadata.xml + local r=$repo/$base_path/maven-metadata.xml + + get $r $l +# x=`xmlstarlet sel -t -m '//snapshotVersion[extension[text()="zip"]]' -v value $l` + set -- `xmlstarlet sel -t -m '/metadata/versioning/snapshot' -v "timestamp|buildNumber" $l` + snapshot_version="$1-$2" + + if [[ $snapshot_version == "" ]] + then + echo "Unable to resolve SNAPSHOT version for $group_id:$artifact_id:$version" + exit 1 + fi + + app-conf set maven.snapshotVersion "$snapshot_version" + app-conf set app.version "${version%-SNAPSHOT}-$snapshot_version" +} + +download_version() { + resolved_version="" + target="" + while getopts "v:f:" opt + do + case $opt in + v) + resolved_version="$OPTARG" + ;; + f) + target="$OPTARG" + ;; + *) + usage "Invalid option: $OPTARG" + ;; + esac + done + + if [[ $resolved_version == "" || $target == "" ]] + then + usage + fi + + repo=`app-conf get maven.repo` + group_id=`app-conf get maven.group_id` + artifact_id=`app-conf get maven.artifact_id` + version=`app-conf get maven.version` + + group_id_slash=`slash $group_id` + base_path=$group_id_slash/$artifact_id/$version + + mkdir -p .app/cache/$base_path + + l=.app/cache/$base_path/$artifact_id-$resolved_version.zip + r=$repo/$base_path/$artifact_id-$resolved_version.zip + + echo "Downloading $group_id:$artifact_id:$resolved_version..." + get $r $l + + ln -s "`pwd`/$l" "$target" +} + +init() { + while getopts "r:" opt + do + case $opt in + r) + app-conf set maven.repo "$OPTARG" + shift 2 + OPTIND=1 + ;; + *) + usage "Invalid option: $OPTARG" + ;; + esac + done + + x=`echo $1 | tr ":" " "`; set -- $x + group_id=$1 + artifact_id=$2 + version=$3 + + if [ -z "$group_id" -o -z "$artifact_id" -o -z "$version" ] + then + usage "Invalid Maven coordinates: $coordinates" + fi + + app-conf set maven.group_id "$group_id" + app-conf set maven.artifact_id "$artifact_id" + app-conf set maven.version "$version" +} + +subapp="$0"; +command="$1"; shift + +case "$command" in + init) + init "$@" + ;; + resolve-version) + resolve_version + ;; + download-version) + download_version "$@" + ;; + *) + usage + ;; +esac + +exit 0 diff --git a/test/app-cat-conf.bats b/test/app-cat-conf.bats new file mode 100755 index 0000000..a95049a --- /dev/null +++ b/test/app-cat-conf.bats @@ -0,0 +1,56 @@ +#!/usr/bin/env bats +# vim: set filetype=sh: + +load utils + +setup_inner() { + export APPSH_DEFAULT_CONFIG=/dev/null +} + +@test "app-cat-conf" { + app_libexec app-cat-conf -f $APPSH_HOME/test/data/app-cat-conf/config-1 + echo_lines + eq '${lines[0]}' "baz.kiz=zap" + eq '${lines[1]}' "baz.wat=baz" + eq '${lines[2]}' "foo.bar=wat" + eq '${lines[3]}' "foo.baz=kaz" + eq '${lines[4]}' "foo.wat=foo" + eq '${#lines[*]}' 5 +} + +@test "app-cat-conf -g baz" { + app_libexec app-cat-conf -f $APPSH_HOME/test/data/app-cat-conf/config-1 -n "baz\..*" + echo_lines + eq '${lines[0]}' "baz.kiz=zap" + eq '${lines[1]}' "baz.wat=baz" + eq '${#lines[*]}' 2 +} + +@test "app-cat-conf -k wat" { + app_libexec app-cat-conf -f $APPSH_HOME/test/data/app-cat-conf/config-1 -n ".*\.wat" + echo_lines + eq '${lines[0]}' "baz.wat=baz" + eq '${lines[1]}' "foo.wat=foo" + eq '${#lines[*]}' 2 +} + +@test "app-cat-conf -g baz -k wat" { + app_libexec app-cat-conf -f $APPSH_HOME/test/data/app-cat-conf/config-1 -n "baz\.wat" + echo_lines + eq '${lines[0]}' "baz.wat=baz" + eq '${#lines[*]}' 1 +} + +@test "uses \$APPSH_DEFAULT_CONFIG" { + APPSH_DEFAULT_CONFIG=$APPSH_HOME/test/data/app-cat-conf/config-2 + app_libexec app-cat-conf -f /dev/null + echo_lines + eq '${lines[0]}' "foo.bar=wat" + eq '${#lines[*]}' 1 + + app_libexec app-cat-conf -f $APPSH_HOME/test/data/app-cat-conf/config-3 + echo_lines + eq '${lines[0]}' "foo.bar=baz" + eq '${lines[1]}' "foo.wat=bar" + eq '${#lines[*]}' 2 +} diff --git a/test/app-common.bats b/test/app-common.bats new file mode 100755 index 0000000..d2173f5 --- /dev/null +++ b/test/app-common.bats @@ -0,0 +1,14 @@ +#!/usr/bin/env bats +# vim: set filetype=sh: + +load utils + +@test "grep_path" { + . $APPSH_HOME/lib/common + + x=`grep_path "app-.*" "$APPSH_HOME/test/data/app-common:/does-not-exist"|sort|sed s,$APPSH_HOME/,,|xargs` + [[ $x == "test/data/app-common/app-bar test/data/app-common/app-faz test/data/app-common/app-foo" ]] + + x=`grep_path "app-f.*" "$APPSH_HOME/test/data/app-common:/does-not-exist"|sort|sed s,$APPSH_HOME/,,|xargs` + [[ $x == "test/data/app-common/app-faz test/data/app-common/app-foo" ]] +} diff --git a/test/app-conf.bats b/test/app-conf.bats index 10e2757..08676d2 100755 --- a/test/app-conf.bats +++ b/test/app-conf.bats @@ -3,92 +3,117 @@ load utils -@test "./app conf - happy day" { - i=env-a - n=app-a - - mkzip "app-a" - app instance install -r file -u $BATS_TEST_DIRNAME/data/app-a.zip -n $n -i $i -v 1.0 - [ $status -eq 0 ] - - app -n $n -i $i conf; echo_lines - [ $status -eq 0 ] - [ "$output" = "app.bin bin/app-a " ] +setup_inner() { + mkdir .app; touch .app/config + export APPSH_DEFAULT_CONFIG=/dev/null +} - app -n $n -i $i conf set group.foo bar; echo_lines - [ $status -eq 0 ] +@test "./app conf - happy day" { + app conf; echo_lines + echo "app.bin=bin/app-a" > .app/config + eq '$status' 0 + eq '${#lines[*]}' 0 + + app conf set g.foo bar; echo_lines + eq '$status' 0 + + app conf; echo_lines + eq '$status' 0 + eq '${lines[0]}' "app.bin bin/app-a " + eq '${lines[1]}' "g.foo bar " + eq '${#lines[*]}' 2 + + app conf get g.foo; echo_lines + eq '$status' 0 + eq '${lines[0]}' "bar" + eq '${#lines[*]}' 1 + + app conf delete g.foo; echo_lines + eq '$status' 0 + + app conf; echo_lines + eq '$status' 0 + eq '${lines[0]}' "app.bin bin/app-a " + eq '${#lines[*]}' 1 +} - app -n $n -i $i conf; echo_lines - [ $status -eq 0 ] - [ "$output" = "app.bin bin/app-a -group.foo bar " ] +@test "./app conf - defaults to 'list'" { + echo "app.bin=bin/app-a" > .app/config - app -n $n -i $i conf delete group.foo; echo_lines - [ $status -eq 0 ] + app conf; echo_lines + eq '$status' 0 + eq '${#lines[*]}' 1 + eq '${lines[0]}' "app.bin bin/app-a " +} - app -n $n -i $i conf; echo_lines - [ $status -eq 0 ] - [ "$output" = "app.bin bin/app-a " ] +@test "./app conf wat" { + app conf wat; echo_lines + eq '$status' 1 + eq '${lines[0]}' "Error: Unknown command: wat" } @test "./app conf list" { - i=env-a - n=app-a - - mkzip "app-a" - app instance install -r file -u $BATS_TEST_DIRNAME/data/app-a.zip -n $n -i $i -v 1.0 - [ $status -eq 0 ] + echo "app.bin=bin/app-a" > .app/config - app -n $n -i $i conf; echo_lines - [ $status -eq 0 ] - [ "$output" = "app.bin bin/app-a " ] + app conf; echo_lines + eq '$status' 0 + eq '${#lines[*]}' 1 + eq '${lines[0]}' "app.bin bin/app-a " - app -n $n -i $i conf list; echo_lines - [ $status -eq 0 ] - [ "$output" = "app.bin bin/app-a " ] + app conf list; echo_lines + eq '$status' 0 + eq '${#lines[*]}' 1 + eq '${lines[0]}' "app.bin bin/app-a " - app -n $n -i $i conf list foo; echo_lines - [ $status -eq 1 ] + app conf list foo; echo_lines + eq '$status' 1 } -@test "./app conf list-group" { - i=env-a - n=app-a - - mkzip "app-a" - app instance install -r file -u $BATS_TEST_DIRNAME/data/app-a.zip -n $n -i $i -v 1.0 - [ $status -eq 0 ] - - app -n $n -i $i conf set mygroup.a 1 - [ $status -eq 0 ] - app -n $n -i $i conf set mygroup.b 1 - [ $status -eq 0 ] - app -n $n -i $i conf set mygroup.c 2 - [ $status -eq 0 ] - app -n $n -i $i conf set othergroup.a 1 - [ $status -eq 0 ] - - app -n $n -i $i conf list-group mygroup; echo_lines - [ $status -eq 0 ] - [ "$output" = "a 1 -b 1 -c 2 " ] -} +#@test "./app conf list-group" { +# app conf set mygroup a 1 +# eq '$status' 0 +# app conf set mygroup b 1 +# eq '$status' 0 +# app conf set mygroup c 2 +# eq '$status' 0 +# app conf set othergroup a 1 +# eq '$status' 0 +# +# app conf list; echo_lines +# eq '$status' 0 +# app conf list-group mygroup; echo_lines +# eq '$status' 0 +# eq '${lines[0]}' "mygroup.a 1 " +# eq '${lines[1]}' "mygroup.b 1 " +# eq '${lines[2]}' "mygroup.c 2 " +# eq '${#lines[*]}' 3 +#} @test "./app conf set" { - i=env-a - n=app-a + echo "app.bin=bin/app-a" > .app/config - mkzip "app-a" - app instance install -r file -u $BATS_TEST_DIRNAME/data/app-a.zip -n $n -i $i -v 1.0 - [ $status -eq 0 ] + app conf set group; echo_lines + eq '$status' 1 - app -n $n -i $i conf set group; echo_lines - [ $status -eq 1 ] + app conf set group.foo; echo_lines + eq '$status' 1 + + app conf set group.foo bar; echo_lines + eq '$status' 0 + eq '${#lines[*]}' 0 + + app conf; echo_lines + eq '$status' 0 + eq '${lines[0]}' "app.bin bin/app-a " + eq '${lines[1]}' "group.foo bar " + eq '${#lines[*]}' 2 +} - app -n $n -i $i conf set group.foo; echo_lines - [ $status -eq 1 ] +@test "./app conf list - with duplicate entries" { + echo "foo.bar=awesome" > .app/config + echo "foo.bar=awesome" >> .app/config - app -n $n -i $i conf set group.foo bar; echo_lines - [ $status -eq 0 ] + app conf list; echo_lines + eq '$status' 0 + eq '${lines[0]}' "foo.bar awesome " } diff --git a/test/app-init.bats b/test/app-init.bats new file mode 100755 index 0000000..b62c42e --- /dev/null +++ b/test/app-init.bats @@ -0,0 +1,42 @@ +#!/usr/bin/env bats +# vim: set filetype=sh: + +load utils + +#@test "Invalid resolver" { +# app init -d my-app wat; echo_lines +# eq '$status' 1 +# eq '${#lines[*]}' 1 +# eq '${lines[0]}' "No such resolver: wat" +#} + +#@test "Already installed" { +# mkdir -p my-app/.apps +# app init -d my-app maven; echo_lines +# eq '$status' 1 +# eq '${#lines[*]}' 1 +# match '${lines[0]}' "my-app" +#} + +@test "Happy day" { + mkzip app-a + + REPO=$BATS_TMPDIR/repo + + if [ ! -f $REPO/org/example/app-a/1.0-SNAPSHOT/maven-metadata.xml ] + then + mvn deploy:deploy-file -Durl=file://$REPO \ + -Dfile=`echo $APPSH_HOME/test/data/app-a.zip` -DgeneratePom \ + -DgroupId=org.example -DartifactId=app-a -Dversion=1.0-SNAPSHOT -Dpackaging=zip + fi + + app init -d my-app maven -r "file://$BATS_TMPDIR/repo" org.example:app-a:1.0-SNAPSHOT; echo_lines + eq '$status' 0 + eq '${lines[0]}' "Resolving version 1.0-SNAPSHOT..." + match '${lines[1]}' "Resolved version to 1.0-.*" + match '${lines[2]}' "Downloading org.example:app-a:1.0-.*" + + eq '${#lines[*]}' 3 + + is_directory "my-app/.app" +} diff --git a/test/app-install.bats b/test/app-install.bats index 38304be..18a84bc 100755 --- a/test/app-install.bats +++ b/test/app-install.bats @@ -5,12 +5,11 @@ load utils # TODO: Add test for installing duplicate version -@test "./app instance install app-a" { +@test "./app install app-a" { mkzip "app-a" - app instance install \ + app install \ -r file \ - -u $BATS_TEST_DIRNAME/data/app-a.zip \ - -n app-a -i prod + -u $BATS_TEST_DIRNAME/data/app-a.zip echo_lines [ $status -eq 0 ] @@ -29,7 +28,7 @@ Postinstall completed successfully" ] app instance install \ -r file \ -u $BATS_TEST_DIRNAME/data/install-test-env.zip \ - -n install-test-env -i prod -v 1.0 + -v 1.0 echo_lines [ $status -eq 0 ] [ "$output" = "Creating instance 'prod' for 'install-test-env' diff --git a/test/data/app-cat-conf/config-1 b/test/data/app-cat-conf/config-1 new file mode 100644 index 0000000..9d78d45 --- /dev/null +++ b/test/data/app-cat-conf/config-1 @@ -0,0 +1,7 @@ + foo.bar = wat +foo.bar=wat +foo.baz=kaz +foo.wat=foo + +baz.wat=baz +baz.kiz=zap diff --git a/test/data/app-cat-conf/config-2 b/test/data/app-cat-conf/config-2 new file mode 100644 index 0000000..288815d --- /dev/null +++ b/test/data/app-cat-conf/config-2 @@ -0,0 +1 @@ +foo.bar=wat diff --git a/test/data/app-cat-conf/config-3 b/test/data/app-cat-conf/config-3 new file mode 100644 index 0000000..569ea27 --- /dev/null +++ b/test/data/app-cat-conf/config-3 @@ -0,0 +1,4 @@ +# same key as config-2, different value +foo.bar=baz + +foo.wat=bar diff --git a/test/data/app-common/app-bar b/test/data/app-common/app-bar new file mode 100755 index 0000000..e69de29 diff --git a/test/data/app-common/app-faz b/test/data/app-common/app-faz new file mode 100755 index 0000000..e69de29 diff --git a/test/data/app-common/app-foo b/test/data/app-common/app-foo new file mode 100755 index 0000000..e69de29 diff --git a/test/utils.bash b/test/utils.bash index cd21966..fa2602e 100644 --- a/test/utils.bash +++ b/test/utils.bash @@ -6,21 +6,25 @@ workdir=test-run exit_usage=1 exit_usage_wrong=0 -echo_lines() { - for line in "${lines[@]}"; do echo $line; done - echo status=$status -} - -APPSH=$(pwd)/app - setup() { - APPSH_APPS=$BATS_TMPDIR/app.sh - APPSH_HOME=$(cd $BATS_TEST_DIRNAME/../..; echo `pwd`/app.sh) - APPSH_APPS_CANONICAL=$(cd -P $APPSH_APPS; pwd) + PATH=/bin:/usr/bin + PATH=$PATH:$APPSH_HOME + APPSH_HOME=$(cd $BATS_TEST_DIRNAME/..; echo `pwd`) + rm -rf $BATS_TMPDIR/app.sh mkdir $BATS_TMPDIR/app.sh cd $BATS_TMPDIR/app.sh - ln -s $APPSH + + if [ "`declare -f setup_inner >/dev/null; echo $?`" = 0 ] + then + setup_inner + fi +} + +echo_lines() { + echo lines: + for line in "${lines[@]}"; do echo $line; done + echo status=$status } mkzip() { @@ -32,8 +36,16 @@ mkzip() { } app() { - echo ./app $@ - run ./app $@ + echo app $@ + run $APPSH_HOME/app $@ +} + +app_libexec() { + local x=`PATH=$APPSH_HOME/libexec:/bin:/usr/bin which $1` + + echo libexec/$@ + shift + run "$x" $@ } describe() { @@ -59,3 +71,43 @@ can_not_read() { return 1 fi } + +is_directory() { + if [ ! -d "$1" ] + then + echo "Not a directory: $1" 2>&1 + return 1 + fi +} + +eq() { + local ex="$1" + local e="$2" + local a="`eval echo $ex`" + + if [[ $e == $a ]] + then + return 0 + fi + + echo "Assertion failed: $ex" + echo "Expected: $e" + echo "Actual: $a" + exit 1 +} + +match() { + local ex="$1" + local regex="$2" + local a="`eval echo $ex`" + + if [[ $a =~ $regex ]] + then + return 0 + fi + + echo "Assertion failed: $ex =~ $a" + echo "Expected: $e" + echo "Actual: $a" + exit 1 +} -- cgit v1.2.3