#!/bin/bash # ====================================================================== # # DOCKER PHP DEV ENVIRONMENT :: INIT # # ---------------------------------------------------------------------- # 2021-11-nn v1.0 <axel.hahn@iml.unibe.ch> # 2022-07-19 v1.1 <axel.hahn@iml.unibe.ch> support multiple dirs for setfacl # 2022-11-16 v1.2 <www.axel-hahn.de> use docker-compose -p "$APP_NAME" # 2022-12-18 v1.3 <www.axel-hahn.de> add -p "$APP_NAME" in other docker commands # 2022-12-20 v1.4 <axel.hahn@unibe.ch> replace fgrep with grep -F # 2023-03-06 v1.5 <www.axel-hahn.de> up with and without --build # 2023-08-17 v1.6 <www.axel-hahn.de> menu selection with single key (without return) # 2023-11-10 v1.7 <axel.hahn@unibe.ch> replace docker-compose with "docker compose" # 2023-11-13 v1.8 <axel.hahn@unibe.ch> UNDO "docker compose"; update infos # 2023-11-15 v1.9 <axel.hahn@unibe.ch> add help; execute multiple actions by params; new menu item: open app # 2023-12-07 v1.10 <www.axel-hahn.de> simplyfy console command; add php linter # 2024-07-01 v1.11 <www.axel-hahn.de> diff with colored output; suppress errors on port check # 2024-07-19 v1.12 <axel.hahn@unibe.ch> apply shell fixes # 2024-07-22 v1.13 <axel.hahn@unibe.ch> show info if there is no database container; speedup replacements # 2024-07-22 v1.14 <axel.hahn@unibe.ch> show colored boxes with container status # 2024-07-24 v1.15 <axel.hahn@unibe.ch> update menu output # ====================================================================== cd "$( dirname "$0" )" || exit 1 # init used vars gittarget= frontendurl= _self=$( basename "$0" ) # shellcheck source=/dev/null . "${_self}.cfg" || exit 1 _version="1.15" # git@git-repo.iml.unibe.ch:iml-open-source/docker-php-starterkit.git selfgitrepo="docker-php-starterkit.git" fgGray="\e[1;30m" fgRed="\e[31m" fgGreen="\e[32m" fgBrown="\e[33m" fgBlue="\e[34m" fgReset="\e[0m" # ---------------------------------------------------------------------- # FUNCTIONS # ---------------------------------------------------------------------- # draw a headline 2 function h2(){ echo echo -e "$fgBrown>>>>> $*$fgReset" } # draw a headline 3 function h3(){ echo echo -e "$fgBlue----- $*$fgReset" } # show help for param -h function showMenu(){ cat <<EOM $( _key g ) - remove git data of starterkit $( _key i ) - init application: set permissions $( _key t ) - generate files from templates $( _key T ) - remove generated files $( _key u ) - startup containers docker-compose ... up -d $( _key U ) - startup containers docker-compose ... up -d --build $( _key s ) - shutdown containers docker-compose stop $( _key r ) - remove containers docker-compose rm -f $( _key m ) - more infos $( _key o ) - open app [${APP_NAME}] $frontendurl $( _key c ) - console (bash) $( _key p ) - console check with php linter $( _key q ) - quit EOM } function showHelp(){ cat <<EOH INITIALIZER FOR DOCKER APP v$_version A helper script written in Bash to bring up a PHP+Mysql application in docker. Source : https://git-repo.iml.unibe.ch/iml-open-source/docker-php-starterkit Docs : https://os-docs.iml.unibe.ch/docker-php-starterkit/ License: GNU GPL 3.0 (c) Institute for Medical Education; University of Bern SYNTAX: $_self [-h|-v] $_self [menu key] OPTIONS: -h show this help and exit -v show version exit MENU KEYS: In the interactive menu are some keys to init an action. The same keys can be put as parameter to start this action. You can add multiples keys to apply multiple actions. $( showMenu ) EXAMPLES: $_self starts interactive mode $_self u bring up docker container(s) and stay in interactive mode $_self i q set write permissions and quit $_self p q start php linter and exit EOH } # set acl on local directory function _setWritepermissions(){ h2 "set write permissions on ${gittarget} ..." local _user; _user=$( id -gn ) local _user_uid; typeset -i _user_uid=0 test -f /etc/subuid && _user_uid=$( grep "$_user" /etc/subuid 2>/dev/null | cut -f 2 -d ':' )-1 local DOCKER_USER_OUTSIDE; typeset -i DOCKER_USER_OUTSIDE=$_user_uid+$DOCKER_USER_UID set -vx for mywritedir in ${WRITABLEDIR} do echo "--- ${mywritedir}" # remove current acl sudo setfacl -bR "${mywritedir}" # default permissions: both the host user and the user with UID 33 (www-data on many systems) are owners with rwx perms sudo setfacl -dRm "u:${DOCKER_USER_OUTSIDE}:rwx,${_user}:rwx" "${mywritedir}" # permissions: make both the host user and the user with UID 33 owner with rwx perms for all existing files/directories sudo setfacl -Rm "u:${DOCKER_USER_OUTSIDE}:rwx,${_user}:rwx" "${mywritedir}" done set +vx } # cleanup starterkit git data function _removeGitdata(){ h2 "Remove git data of starterkit" echo -n "Current git remote url: " git config --get remote.origin.url if git config --get remote.origin.url 2>/dev/null | grep $selfgitrepo >/dev/null; then echo echo -n "Delete local .git and .gitignore? [y/N] > " read -r answer test "$answer" = "y" && ( echo "Deleting ... " && rm -rf ../.git ../.gitignore ) else echo "It was done already - $selfgitrepo was not found." fi } # helper function: cut a text file starting from database start marker # see _generateFiles() function _fix_no-db(){ local _file=$1 if [ "$DB_ADD" = "false" ]; then local iStart; typeset -i iStart iStart=$( grep -Fn "$CUTTER_NO_DATABASE" "${_file}" | cut -f 1 -d ':' )-1 if [ $iStart -gt 0 ]; then sed -ni "1,${iStart}p" "${_file}" fi fi } # helper functiion to generate replacements using sed # it loops over all vars in the config file # used in _generateFiles function _getreplaces(){ # loop over vars to make the replacement grep "^[a-zA-Z]" "$_self.cfg" | while read -r line do # echo replacement: $line mykey=$( echo "$line" | cut -f 1 -d '=' ) myvalue="$( eval echo \"\$"$mykey"\" )" # TODO: multiline values fail here in replacement with sed echo -e "s#{{$mykey}}#${myvalue}#g" done } # loop over all files in templates subdir make replacements and generate # a target file. # It skips if # - 1st line is not starting with "# TARGET: filename" # - target file has no updated lines function _generateFiles(){ # shellcheck source=/dev/null . "${_self}.cfg" || exit 1 params=$( _getreplaces | while read -r line; do echo -n "-e '$line' "; done ) local _tmpfile=/tmp/newfilecontent$$.tmp h2 "generate files from templates..." for mytpl in templates/* do # h3 $mytpl local _doReplace=1 # fetch traget file from first line target=$( head -1 "$mytpl" | grep "^# TARGET:" | cut -f 2- -d ":" | awk '{ print $1 }' ) if [ -z "$target" ]; then echo "SKIP: $mytpl - target was not found in 1st line" _doReplace=0 fi # write generated files to target if [ $_doReplace -eq 1 ]; then # write file from line 2 to a tmp file sed -n '2,$p' "$mytpl" >"$_tmpfile" # add generator # sed -i "s#{{generator}}#generated by $0 - template: $mytpl - $( date )#g" $_tmpfile local _md5; _md5=$( md5sum $_tmpfile | awk '{ print $1 }' ) sed -i "s#{{generator}}#GENERATED BY $_self - template: $mytpl - $_md5#g" $_tmpfile eval sed -i "$params" "$_tmpfile" || exit _fix_no-db $_tmpfile # echo "changes for $target:" if diff --color=always "../$target" "$_tmpfile" | grep -v "$_md5" | grep -v "^---" | grep . || [ ! -f "../$target" ]; then echo -n "$mytpl - changes detected - writing [$target] ... " mkdir -p "$( dirname ../"$target" )" || exit 2 mv "$_tmpfile" "../$target" || exit 2 echo OK echo else rm -f $_tmpfile echo "SKIP: $mytpl - Nothing to do." fi fi done } # loop over all files in templates subdir make replacements and generate # a traget file. function _removeGeneratedFiles(){ h2 "remove generated files..." for mytpl in templates/* do h3 "$mytpl" # fetch traget file from first line target=$( head -1 "$mytpl" | grep "^# TARGET:" | cut -f 2- -d ":" | awk '{ print $1 }' ) if [ -n "$target" ] && [ -f "../$target" ]; then echo -n "REMOVING " ls -l "../$target" || exit 2 rm -f "../$target" || exit 2 echo OK else echo "SKIP: $target" fi done } # show running containers function _showContainers(){ local bLong=$1 local _out local sUp=".. UP" local sDown=".. down" local Status= local StatusWeb="$sDown" local StatusDb="" local colWeb= local colDb= colDb="$fgRed" colWeb="$fgRed" _out=$( if [ -z "$bLong" ]; then docker-compose -p "$APP_NAME" ps else # docker ps | grep "$APP_NAME" docker-compose -p "$APP_NAME" ps fi) h2 CONTAINERS if [ "$( wc -l <<< "$_out" )" -eq 1 ]; then if [ "$DB_ADD" = "false" ]; then colDb="$fgGray" Status="The web container is not running. This app has no database container." else StatusDb="down" Status="No container is running for <$APP_NAME>." fi STATUS_CONTAINERS_UP=1 else grep -q "${APP_NAME}-server" <<< "$_out" && colWeb="$fgGreen" grep -q "${APP_NAME}-server" <<< "$_out" && StatusWeb="$sUp" grep -q "${APP_NAME}-db" <<< "$_out" && colDb="$fgGreen" StatusDb="$sDown" grep -q "${APP_NAME}-db" <<< "$_out" && StatusDb="$sUp" if [ "$DB_ADD" = "false" ]; then colDb="$fgGray" Status="INFO: This app has no database container." fi fi printf "$colWeb __________________________ $colDb __________________________ $fgReset \n" printf "$colWeb | %-22s | $colDb | %-22s | $fgReset \n" "" "" printf "$colWeb | %-22s | $colDb | %-22s | $fgReset \n" "${APP_NAME}-web ${StatusWeb}" "${APP_NAME}-db ${StatusDb}" printf "$colWeb | %-22s | $colDb | %-22s | $fgReset \n" " PHP ${APP_PHP_VERSION}" " ${MYSQL_IMAGE}" printf "$colWeb | %-22s | $colDb | %-22s | $fgReset \n" " :${APP_PORT}" " :${DB_PORT}" printf "$colWeb |__________________________| $colDb |__________________________| $fgReset \n" if [ -n "$Status" ]; then echo echo "$Status" fi echo if [ -n "$bLong" ]; then echo "$_out" h2 STATS docker stats --no-stream echo fi } # show urls for app container function _showBrowserurl(){ echo "In a web browser open:" echo " $frontendurl" if grep "${APP_NAME}-server" /etc/hosts >/dev/null; then echo " https://${APP_NAME}-server/" fi } # detect + show ports and urls for app container and db container function _showInfos(){ _showContainers long h2 INFO h3 "processes webserver" # docker-compose top docker top "${APP_NAME}-server" if [ ! "$DB_ADD" = "false" ]; then h3 "processes database" docker top "${APP_NAME}-db" fi h3 "Check app port" if echo >"/dev/tcp/localhost/${APP_PORT}"; then echo "OK, app port ${APP_PORT} is reachable" echo _showBrowserurl else echo "NO, app port ${APP_PORT} is not available" fi 2>/dev/null if [ "$DB_ADD" != "false" ]; then h3 "Check database port" if echo >"/dev/tcp/localhost/${DB_PORT}"; then echo "OK, db port ${DB_PORT} is reachable" echo echo "In a local DB admin tool you can connect it:" echo " host : localhost" echo " port : ${DB_PORT}" echo " user : root" echo " password: ${MYSQL_ROOT_PASS}" else echo "NO, db port ${DB_PORT} is not available" fi 2>/dev/null fi echo } # helper for menu: print an inverted key function _key(){ echo -en "\e[4;7m ${1} \e[0m" } # helper: wait for a return key function _wait(){ local _wait=15 echo -n "... press RETURN ... or wait $_wait sec > "; read -r -t $_wait } # ---------------------------------------------------------------------- # MAIN # ---------------------------------------------------------------------- action=$1; shift 1 while true; do if [ -z "$action" ]; then echo "_______________________________________________________________________________" echo echo echo " ${APP_NAME^^} :: Initializer for docker" echo " ______" echo "________________________________________________________________________/ $_version" echo _showContainers h2 MENU showMenu echo echo -n " select >" read -rn 1 action echo fi case "$action" in "-h") showHelp; exit 0 ;; "-v") echo "$_self $_version"; exit 0 ;; g) _removeGitdata ;; i) # _gitinstall _setWritepermissions ;; t) _generateFiles ;; T) _removeGeneratedFiles rm -rf containers ;; m) _showInfos _wait ;; u|U) h2 "Bring up..." dockerUp="docker-compose -p $APP_NAME --verbose up -d --remove-orphans" if [ "$action" = "U" ]; then dockerUp+=" --build" fi echo "$dockerUp" if $dockerUp; then _showBrowserurl else echo "ERROR: docker-compose up failed :-/" docker-compose -p "$APP_NAME" logs | tail fi echo _wait ;; s) h2 "Stopping..." docker-compose -p "$APP_NAME" stop ;; r) h2 "Removing..." docker-compose -p "$APP_NAME" rm -f ;; c) h2 "Console" _containers=$( docker-compose -p "$APP_NAME" ps | sed -n "2,\$p" | awk '{ print $1}' ) if [ "$DB_ADD" = "false" ]; then dockerid=$_containers else echo "Select a container:" sed "s#^# #g" <<< "$_containers" echo -n "id or name >" read -r dockerid fi test -z "$dockerid" || ( echo echo "> docker exec -it $dockerid /bin/bash (type 'exit' + Return when finished)" docker exec -it "$dockerid" /bin/bash ) ;; p) h2 "PHP $APP_PHP_VERSION linter" dockerid="${APP_NAME}-server" echo -n "Scanning ... " typeset -i _iFiles _iFiles=$( docker exec -it "$dockerid" /bin/bash -c "find . -name '*.php' " | wc -l ) if [ $_iFiles -gt 0 ]; then echo "found $_iFiles [*.php] files ... errors from PHP $APP_PHP_VERSION linter:" time if echo "$APP_PHP_VERSION" | grep -E "([567]\.|8\.[012])" >/dev/null ; then docker exec -it "$dockerid" /bin/bash -c "find . -name '*.php' -exec php -l {} \; | grep -v '^No syntax errors detected'" else docker exec -it "$dockerid" /bin/bash -c "php -l \$( find . -name '*.php' ) | grep -v '^No syntax errors detected' " fi echo _wait else echo "Start your docker container first." fi ;; o) h2 "Open app ..." xdg-open "$frontendurl" ;; q) h2 "Bye!" exit 0; ;; *) test -n "$action" && ( echo " ACTION FOR [$action] NOT IMPLEMENTED."; sleep 1 ) esac action=$1; shift 1 done # ----------------------------------------------------------------------