#!/usr/bin/env bash FIN_VERSION=1.65.0 if [[ "$TERM" != "dumb" ]]; then # Console colors red='\033[0;91m' red_bg='\033[101m' yellow_bg='\033[43;90m' green='\033[0;32m' green_bg='\033[42m' yellow='\033[0;33m' yellow_bold='\033[1;33m' blue='\033[0;34m' lime='\033[0;92m' acqua='\033[0;96m' magenta='\033[0;35m' NC='\033[0m' fi #-------------------------------- OS Checks ------------------------------------ # OS version detection if [[ -f "/etc/os-release" ]]; then (uname -a | grep -v 'Microsoft' >/dev/null) && OS_TYPE="Linux" || OS_TYPE="WSL" IFS=";" read OS_NAME OS_VERSION OS_ID OS_ID_LIKE < <(source "/etc/os-release"; echo "$NAME;$VERSION_ID;$ID;$ID_LIKE") export OS_TYPE OS_NAME OS_VERSION elif (uname | grep 'Darwin' >/dev/null); then export OS_TYPE="Darwin" export OS_NAME="$(sw_vers -productName)" export OS_VERSION="$(sw_vers -productVersion)" elif (uname | grep 'CYGWIN_NT' >/dev/null); then export OS_TYPE="Cygwin" export OS_NAME="Windows" export OS_VERSION="$(echo $(cmd /c ver) | sed 's/.*Version \(.*\)\..*]/\1/')" fi is_linux () { [[ "$OS_TYPE" == "Linux" ]] } is_debian () { [[ "$OS_ID" == "ubuntu" ]] || [[ "$OS_ID" == "debian" ]] || [[ "$OS_ID_LIKE" == "ubuntu" ]] || [[ "$OS_ID_LIKE" == "debian" ]] } is_alpine () { [[ "$OS_ID" == "alpine" ]] } is_fedora () { [[ "$OS_ID" == "fedora" ]] || [[ "$OS_ID" == "centos" ]] } is_windows () { [[ "$OS_TYPE" == "Cygwin" ]] } is_wsl () { [[ "$OS_TYPE" == "WSL" ]] } is_mac () { [[ "$OS_TYPE" == "Darwin" ]] } # CI is_ci () { [[ "$CI" != "" ]] } # Play with Docker is_pwd () { [[ "$(cat /etc/motd 2>/dev/null)" =~ "The PWD team" ]] } # Katacoda is_katacoda () { # There is no standard way to detect a Katacoda environment, so we have to rely on on a variable [[ "$KATACODA" != "" ]] } is_travis () { [[ "$TRAVIS" == "true" ]] } #------------------------------------------------------------------------------ #---------------------------- Global Constants -------------------------------- # Allow to override Docksal version during install. # Install: curl -L https://get.docksal.io | DOCKSAL_VERSION=develop bash # Update: DOCKSAL_VERSION=develop fin update DOCKSAL_VERSION="${DOCKSAL_VERSION:-master}" # Dependency versions REQUIREMENTS_DOCKER='18.03.1-ce' REQUIREMENTS_DOCKER_COMPOSE='1.21.1' REQUIREMENTS_DOCKER_MACHINE='0.14.0' REQUIREMENTS_VBOX='5.2.2' # Remember to update the download URLs below REQUIREMENTS_WINPTY='0.4.3' REQUIREMENTS_WINPTY_CYGWIN='2.8.0' # VirtualBox download URLs URL_VBOX_MAC="http://download.virtualbox.org/virtualbox/5.2.2/VirtualBox-5.2.2-119230-OSX.dmg" URL_VBOX_WIN="http://download.virtualbox.org/virtualbox/5.2.2/VirtualBox-5.2.2-119230-Win.exe" # Self Paths FIN_PATH="/usr/local/bin/fin" FIN_PATH_UPDATED="/usr/local/bin/fin.updated" FIN_AUTOCOMPLETE_PATH="/usr/local/bin/fin-bash-autocomplete" # Configuration paths # Rewrite $HOME on windows to be absolute path in Unix notation if is_windows; then BABUN_WIN_HOME="$HOME" # Cannot use cygpath_abs_unix here as it's being defined much later HOME="/$(cygpath -m $HOME | sed 's/^\/cygdrive//' | sed 's/\([A-Z]\)\:/\l\1/')" fi CONFIG_DIR="$HOME/.docksal" CONFIG_ENV="$CONFIG_DIR/docksal.env" CONFIG_ALIASES="$CONFIG_DIR/alias" CONFIG_LAST_CHECK="$CONFIG_DIR/.last_check" CONFIG_LAST_PING="$CONFIG_DIR/.last_ping" CONFIG_MACHINES="$CONFIG_DIR/machines" CONFIG_MACHINES_ENV="$CONFIG_DIR/machines/.env" CONFIG_MACHINE_ACTIVE="$CONFIG_MACHINES/.active" mkdir -p "$CONFIG_PROJECTS" >/dev/null 2>&1 # BIN folder CONFIG_BIN_DIR="$CONFIG_DIR/bin" CONFIG_DOWNLOADS_DIR="$CONFIG_BIN_DIR/downloads" DOCKER_BIN="$CONFIG_BIN_DIR/docker" DOCKER_COMPOSE_BIN="$CONFIG_BIN_DIR/docker-compose" DOCKER_COMPOSE_BIN_NIX="/usr/local/bin/docker-compose" DOCKER_MACHINE_BIN="$CONFIG_BIN_DIR/docker-machine" DOCKER_MACHINE_BIN_NIX="/usr/local/bin/docker-machine" WINPTY_BIN="$CONFIG_BIN_DIR/winpty" vboxmanage="VBoxManage" is_windows && vboxmanage="/cygdrive/c/Program Files/Oracle/VirtualBox/VBoxManage.exe" # Stacks folder CONFIG_STACKS_DIR="$HOME/.docksal/stacks" mkdir -p "$CONFIG_STACKS_DIR" >/dev/null 2>&1 # Custom certs folder for vhost-proxy (overridable) CONFIG_CERTS=${CONFIG_CERTS:-$CONFIG_DIR/certs} # Where custom commands live (relative path) DOCKSAL_COMMANDS_PATH=".docksal/commands" # Where addons live (relative path) DOCKSAL_ADDONS_PATH=".docksal/addons" # Docksal environment DOCKSAL_ENVIRONMENT="${DOCKSAL_ENVIRONMENT:-local}" # Network settings export DOCKSAL_IP="192.168.64.100" export DOCKSAL_HOST_IP="192.168.64.1" export DOCKSAL_SUBNET="192.168.64.1/24" # For environments, where access to external DNS servers is blocked, DOCKSAL_DNS_UPSTREAM should be set to the LAN DNS server DOCKSAL_DEFAULT_DNS="8.8.8.8" # For visibility on this variable DOCKSAL_DNS_UPSTREAM="${DOCKSAL_DNS_UPSTREAM}" DOCKSAL_DNS_DOMAIN="${DOCKSAL_DNS_DOMAIN:-docksal}" # Allow disabling the DNS resolver configuration (in case there are issues with it). Set to "true" to activate. DOCKSAL_NO_DNS_RESOLVER="${DOCKSAL_NO_DNS_RESOLVER}" # Set to "true" to enable logging DNS queries in docksal-dns. View logs via "fin docker logs docksal-dns" DOCKSAL_DNS_DEBUG="${DOCKSAL_DNS_DEBUG}" # Declaring possible vhost-proxy settings overrides DOCKSAL_VHOST_PROXY_IP="${DOCKSAL_VHOST_PROXY_IP}" DOCKSAL_VHOST_PROXY_PORT_HTTP="${DOCKSAL_VHOST_PROXY_PORT_HTTP}" DOCKSAL_VHOST_PROXY_PORT_HTTPS="${DOCKSAL_VHOST_PROXY_PORT_HTTPS}" DOCKSAL_VHOST_PROXY_ACCESS_LOG="${DOCKSAL_VHOST_PROXY_ACCESS_LOG}" DOCKSAL_VHOST_PROXY_STATS_LOG="${DOCKSAL_VHOST_PROXY_STATS_LOG}" DOCKSAL_VHOST_PROXY_DEBUG_LOG="${DOCKSAL_VHOST_PROXY_DEBUG_LOG}" PROJECT_INACTIVITY_TIMEOUT="${PROJECT_INACTIVITY_TIMEOUT}" PROJECT_DANGLING_TIMEOUT="${PROJECT_DANGLING_TIMEOUT}" PROJECTS_ROOT="${PROJECTS_ROOT}" DEFAULT_MACHINE_NAME='docksal' DEFAULT_MACHINE_PROVIDER='virtualbox' DEFAULT_MACHINE_VBOX_RAM='2048' #mb DEFAULT_MACHINE_VBOX_HDD='50000' #mb # Stats # fin sends a minimal ping with OS and fin version number DOCKSAL_STATS_TID='UA-93724315-1' DOCKSAL_STATS_URL='http://www.google-analytics.com/collect' DOCKSAL_STATS_OPTOUT=${DOCKSAL_STATS_OPTOUT:-0} # Override PATH to use our utilities PATH="$CONFIG_BIN_DIR:$PATH" # Set global variable in case native Docker app is used/not-used DOCKER_NATIVE="${DOCKER_NATIVE:-0}" #---------------------------- URL references -------------------------------- GITHUB_API="https://api.github.com" URL_REPO="https://raw.githubusercontent.com/docksal/docksal" URL_REPO_UI="https://github.com/docksal/docksal" URL_REPO_DRUPAL7="https://github.com/docksal/drupal7.git" URL_REPO_DRUPAL8="https://github.com/docksal/drupal8.git" URL_REPO_DRUPAL8COMPOSER="https://github.com/docksal/boilerplate-drupal8-composer.git" URL_REPO_WORDPRESS="https://github.com/docksal/wordpress.git" URL_REPO_MAGENTO="https://github.com/docksal/magento.git" URL_REPO_GRAV="https://github.com/docksal/example-grav.git" URL_REPO_GATSBY="https://github.com/docksal/example-gatsby.git" URL_REPO_LARAVEL="https://github.com/docksal/example-laravel.git" URL_REPO_HUGO="https://github.com/docksal/example-hugo.git" URL_REPO_SYMFONY_SKELETON="https://github.com/docksal/example-symfony-skeleton.git" URL_REPO_SYMFONY_WEBAPP="https://github.com/docksal/example-symfony-webapp.git" URL_REPO_BACKDROP="https://github.com/docksal/example-backdrop.git" URL_ADDONS_HOSTING="https://raw.githubusercontent.com" URL_ADDONS_REPO="$URL_ADDONS_HOSTING/docksal/addons" # Remove URL_REPO_ADDONS in Nov 2018 URL_REPO_ADDONS="https://raw.githubusercontent.com/docksal/addons" URL_FIN="${URL_REPO}/${DOCKSAL_VERSION}/bin/fin" URL_STACKS_OVERRIDES_IDE="${URL_REPO}/${DOCKSAL_VERSION}/stacks/overrides-ide.yml" URL_STACKS_OVERRIDES_OSXFS="${URL_REPO}/${DOCKSAL_VERSION}/stacks/overrides-osxfs.yml" URL_STACKS_SERVICES="${URL_REPO}/${DOCKSAL_VERSION}/stacks/services.yml" URL_STACKS_STACK_ACQUIA="${URL_REPO}/${DOCKSAL_VERSION}/stacks/stack-acquia.yml" URL_STACKS_STACK_DEFAULT="${URL_REPO}/${DOCKSAL_VERSION}/stacks/stack-default.yml" URL_STACKS_STACK_DEFAULT_NODB="${URL_REPO}/${DOCKSAL_VERSION}/stacks/stack-default-nodb.yml" URL_STACKS_STACK_NODE="${URL_REPO}/${DOCKSAL_VERSION}/stacks/stack-node.yml" URL_STACKS_VOLUMES_BIND="${URL_REPO}/${DOCKSAL_VERSION}/stacks/volumes-bind.yml" URL_STACKS_VOLUMES_NFS="${URL_REPO}/${DOCKSAL_VERSION}/stacks/volumes-nfs.yml" URL_STACKS_VOLUMES_UNISON="${URL_REPO}/${DOCKSAL_VERSION}/stacks/volumes-unison.yml" URL_DOCKER_MAC="https://download.docker.com/mac/static/stable/x86_64/docker-${REQUIREMENTS_DOCKER}.tgz" URL_DOCKER_NIX="https://get.docker.com/" # Until there is an official standalone docker.exe binary available, use the one pulled from Docker for Windows # See https://github.com/docker/for-win/issues/1460#issuecomment-390045959 #URL_DOCKER_WIN="https://download.docker.com/win/static/stable/x86_64/docker-${REQUIREMENTS_DOCKER}.zip" URL_DOCKER_WIN="https://github.com/docksal/docksal/releases/download/v1.9.0/docker-${REQUIREMENTS_DOCKER}.zip" URL_DOCKER_COMPOSE_MAC="https://github.com/docker/compose/releases/download/${REQUIREMENTS_DOCKER_COMPOSE}/docker-compose-Darwin-x86_64" URL_DOCKER_COMPOSE_NIX="https://github.com/docker/compose/releases/download/${REQUIREMENTS_DOCKER_COMPOSE}/docker-compose-Linux-x86_64" URL_DOCKER_COMPOSE_WIN="https://github.com/docker/compose/releases/download/${REQUIREMENTS_DOCKER_COMPOSE}/docker-compose-Windows-x86_64.exe" URL_DOCKER_MACHINE_MAC="https://github.com/docker/machine/releases/download/v${REQUIREMENTS_DOCKER_MACHINE}/docker-machine-Darwin-x86_64" URL_DOCKER_MACHINE_NIX="https://github.com/docker/machine/releases/download/v${REQUIREMENTS_DOCKER_MACHINE}/docker-machine-Linux-x86_64" URL_DOCKER_MACHINE_WIN="https://github.com/docker/machine/releases/download/v${REQUIREMENTS_DOCKER_MACHINE}/docker-machine-Windows-x86_64.exe" URL_BOOT2DOCKER="https://github.com/boot2docker/boot2docker/releases/download/v${REQUIREMENTS_DOCKER}/boot2docker.iso" URL_WINPTY="https://github.com/rprichard/winpty/releases/download/${REQUIREMENTS_WINPTY}/winpty-${REQUIREMENTS_WINPTY}-cygwin-${REQUIREMENTS_WINPTY_CYGWIN}-ia32.tar.gz" IMAGE_SSH_AGENT=${IMAGE_SSH_AGENT:-docksal/ssh-agent:1.0} IMAGE_VHOST_PROXY=${IMAGE_VHOST_PROXY:-docksal/vhost-proxy:1.2} IMAGE_DNS=${IMAGE_DNS:-docksal/dns:1.0} #---------------------------- Helper functions -------------------------------- DOCKSAL_PATH='' #docksal path value will be cached here echo-red () { echo -e "${red}$1${NC}"; } echo-green () { echo -e "${green}$1${NC}"; } echo-green-bg () { echo -e "${green_bg}$1${NC}"; } echo-yellow () { echo -e "${yellow}$1${NC}"; } echo-warning () { echo -e "${yellow_bg} WARNING: ${NC} ${yellow}$1${NC}"; shift for arg in "$@"; do echo -e " $arg" done } echo-error () { echo -e "${red_bg} ERROR: ${NC} ${red}$1${NC}" shift for arg in "$@"; do echo -e " $arg" done } echo-alert () { echo -e "${red_bg} ALERT: ${NC} ${red}$1${NC}" shift for arg in "$@"; do echo -e " $arg" done } # print string in $1 for $2 times echo-repeat () { seq -f $1 -s '' $2; echo } # prints message to stderr echo-stderr () { (>&2 echo "$@") } # Exits fin if previous command exited with non-zero code if_failed () { if [ ! $? -eq 0 ]; then echo-red "$*" exit 1 fi } # Like if_failed but with more strict error if_failed_error () { if [ ! $? -eq 0 ]; then echo-error "$@" exit 1 fi } # Override default pwd function on windows and remove /cygdrive part from the path pwd () { # -L option should be used at all times because running pwd via absolute path /bin/pwd # on Linux and Windows forces it to resolve current logical dir to physical dir # (resolves symlink into target dir). -L forces using logical dir just like running pwd alone. # We can use `which` here, since it does not care about functions (but `type` does). if is_windows; then $(which pwd) -L | sed 's/^\/cygdrive//' elif is_wsl; then $(which pwd) -L | sed 's/^\/mnt//' else $(which pwd) -L fi } uuid_generate () { od -x /dev/urandom | head -1 | awk '{OFS="-"; print $2$3,$4,$5,$6,$7$8$9}' } docker-compose () { # Mac and Linux use ":"" as a separator, Windows uses ";" local SEPARATOR=':'; is_windows && SEPARATOR=';' docksal_yml_file="$(get_project_path_dc)/.docksal/docksal.yml" # Double check that file really exists here if [[ -f "$docksal_yml_file" ]]; then # This is special hack to make docker-compose work in the context of the project root # https://github.com/docksal/docksal/issues/459 COMPOSE_FILE="${docksal_yml_file}${SEPARATOR}${COMPOSE_FILE}" fi # Call regular docker-compose. # We can use `which` here, since it does not care about functions (but `type` does). "$(which docker-compose)" "$@" } # Returns absolute path to a file/folder in a Unix notation under cygwin on Windows. # cygpath -m returns something like C:/Users/user/... # This function converts that into something like /c/Users/user/... # @param $1 file/folder cygpath_abs_unix () { echo "/$(cygpath -m $1 | sed 's/^\/cygdrive//' | sed 's/\([A-Z]\)\:/\l\1/')" } # Search for a file/directory in a directory tree upwards. Return its path. # @param $1 filename upfind () { if [[ $1 == '' ]]; then return 1; fi local _path _path=$( #incapsulate cd while [[ ! -f $1 ]] && [[ ! -d $1 ]] && [[ $(pwd) != / ]]; do cd ".." done; if [[ -f $1 ]] || [[ -d $1 ]]; then echo $(pwd); exit; fi ) # On Windows compensate for getting down to "" and return full absolute path in Unix notation # upfind on windows may return FAKED HOME PATH! because of that. KEEP that in mind [[ "$_path" == "" ]] && _path="$HOME" if is_windows; then echo "$(cygpath_abs_unix ${_path})" else echo "$_path" fi } # Get path to .docksal folder using upfind get_project_path () { if [ -z "$DOCKSAL_PATH" ]; then DOCKSAL_PATH=$(upfind ".docksal") fi # If we reached $HOME, then we did not find the project root. if [[ "$DOCKSAL_PATH" != "$HOME" ]]; then echo "$DOCKSAL_PATH" fi } # Get path to project folder # Outputs a Windows compatible path on Windows, e.g., "c:/path/subpath", not "/c/path/subpath". get_project_path_dc () { if is_windows ; then local _path="$(get_project_path)" [[ "$_path" != "" ]] && cygpath -m "$_path" else echo "$(get_project_path)" fi } # Get path to global .docksal folder (~/.docksal directory). # Outputs a Windows compatible path on Windows, e.g., "c:/path/subpath", not "/c/path/subpath". get_config_dir_dc () { if is_windows ; then cygpath -m "$CONFIG_DIR" else echo "$CONFIG_DIR" fi } # Returns absolute path # @param $1 file/dir relative path get_abs_path () { local _dir if [[ -f "$1" ]]; then _dir="$(dirname $1)" elif [[ -d "$1" ]]; then _dir="$1" else echo "Path \"$1\" does not exist" return 1 fi echo "$(cd "${_dir}" ; pwd)" } # Return current path relative to project root with trailing slash get_current_relative_path () { # Check that we're inside project folder local proj_root=$(get_project_path) local cwd=$(pwd) # On Windows pwd may return a cygwin absolute path. # We need a full absolute path here (/c/Users/user/... instead of /home/user), thus the special handling. is_windows && cwd=$(cygpath_abs_unix $(pwd)) # Output relative path unless we are in the project root (empty relative path) if [[ "$proj_root" != "$cwd" ]]; then # if cwd substract proj_root is still cwd then it means we're out of proj_root (unsubstractable) # ex: cwd=/a/b/c/d, proj_root=/a/b/c, pathdiff==d # ex: cwd=/a/b, proj_root=/a/b/c, pathdiff==/a/b local pathdiff=${cwd#${proj_root}/} echo "$pathdiff" fi } # Returns addon path if can find it in project or global addons. Otherwise returns empty string # $1 - addon name get_addon_script () { ADDON_ROOT="$(get_project_path)/$DOCKSAL_ADDONS_PATH" # First search project addons dir for the addon command_script="$ADDON_ROOT/$1/$1" # Then search global docksal addons directory for the addon [ ! -f "$command_script" ] && ADDON_ROOT="$command_script" && command_script="$HOME/$DOCKSAL_ADDONS_PATH/$1/$1" # If addon was found then return it [ -f "$command_script" ] && export ADDON_ROOT && echo "$command_script" } # Returns command path if can find it in project or global commands. Otherwise returns empty string # $1 - command name get_command_script () { COMMANDS_ROOT="$(get_project_path)/$DOCKSAL_COMMANDS_PATH" # First search project commands folder for the command command_script="$COMMANDS_ROOT/$1" # Then search global docksal commands directory for the command [ ! -f "$command_script" ] && command_script="$HOME/$DOCKSAL_COMMANDS_PATH/$1" # If command was found then return it [ -f "$command_script" ] && echo "$command_script" } # Get mysql connection string get_mysql_connect () { # Run drush forcing tty to false to avoid colored output string from drush. cleaned_string=$(echo $(_exec drush sql-connect) | sed -e 's/[^a-zA-Z0-9_-]$//') echo "$cleaned_string" } # Get container id by service name # @param $1 docker compose service name (e.g., cli) # @return docker container id # TODO: deprecate in favor of ${COMPOSE_PROJECT_NAME_SAFE}__1 get_container_id () { # Trim any control characters from the output, otherwise there will be issues passing it to the docker binary on Windows. echo $(docker-compose ps -q $1 2>/dev/null | tr -d '[:cntrl:]') } # Run command on Windows with elevated privileges winsudo () { cygstart --action=runas cmd /c "$@" } sudowin () { # left for future use powershell.exe Start-Process -Verb runAs -FilePath "cmd.exe" -Args '/c','dir','/?' } # Universal Bash parameter parsing # Parse equal sign separated params into named local variables # Standalone named parameter value will equal its param name (--force creates variable $force=="force") # Parses multi-valued named params into an array (--path=path1 --path=path2 creates ${path[*]} array) # Parses un-named params into ${ARGV[*]} array # Additionally puts all named params into ${ARGN[*]} array # Additionally puts all standalone "option" params into ${ARGO[*]} array # @author Oleksii Chekulaiev # @version v1.4 (Jun-26-2018) parse_params () { local existing_named local ARGV=() # un-named params local ARGN=() # named params local ARGO=() # options (--params) echo "local ARGV=(); local ARGN=(); local ARGO=();" while [[ "$1" != "" ]]; do # Escape asterisk to prevent bash asterisk expansion _escaped=${1/\*/\'\"*\"\'} # If equals delimited named parameter if [[ "$1" =~ ^..*=..* ]]; then # Add to named parameters array echo "ARGN+=('$_escaped');" # key is part before first = local _key=$(echo "$1" | cut -d = -f 1) # val is everything after key and = (protect from param==value error) local _val="${1/$_key=}" # remove dashes from key name _key=${_key//\-} # skip when key is empty if [[ "$_key" == "" ]]; then shift continue fi # search for existing parameter name if (echo "$existing_named" | grep "\b$_key\b" >/dev/null); then # if name already exists then it's a multi-value named parameter # re-declare it as an array if needed if ! (declare -p _key 2> /dev/null | grep -q 'declare \-a'); then echo "$_key=(\"\$$_key\");" fi # append new value echo "$_key+=('$_val');" else # single-value named parameter echo "local $_key=\"$_val\";" existing_named=" $_key" fi # If standalone named parameter elif [[ "$1" =~ ^\-. ]]; then # remove dashes local _key=${1//\-} # skip when key is empty if [[ "$_key" == "" ]]; then shift continue fi # Add to options array echo "ARGO+=('$_escaped');" echo "local $_key=\"$_key\";" # non-named parameter else # Escape asterisk to prevent bash asterisk expansion _escaped=${1/\*/\'\"*\"\'} echo "ARGV+=('$_escaped');" fi shift done } # Check if dir is empty # $1 - dir path # $2 - -a optional to also search for hidden files empty_dir () { [[ "$2" == "-a" ]] && _all=" -A " || _all="" [[ -z "$(ls ${_all} $1 2>/dev/null)" ]] } #------------------------- Basics check functions ----------------------------- is_docker_native () { # comparison returns error codes [[ "$DOCKER_NATIVE" == "1" ]] } # Check if file has crlf endings # param $1 filename is_crlf () { [[ $(grep -c $'\r' "$1") -gt 0 ]] } # Check if file has crlf endings and fix it # param $1 filename fix_crlf () { CR=$'\r' cat "$1" | sed "s/$CR//" | tee "$1" >/dev/null } # Check if file has crlf endings, warn about it and fix it # param $1 filename fix_crlf_warning () { if (is_crlf "$1"); then if is_tty; then echo -e "${red}WARNING: ${NC}${yellow}$1${NC} has CRLF line endings." echo-red "You should configure your git or repo to always use LF line endings for Docksal files" _confirm "Fix this file automatically?" fi fix_crlf "$1" fi } # checks if binary exists and fails is it isn't check_binary_found () { if ( which "$1" >/dev/null 2>&1 ); then return 0 else echo-red "$1 executable was not found. (Try running 'fin update')" exit 1 fi } check_winpty_found () { ! is_windows && return check_binary_found 'winpty' # -Xallow-non-tty: allow stdin/stdout to not be ttys # This is an undocumented feature of winpty which makes thing work much better on Windows, # including pipes (|), stream redirects (< >) and variable substitution from a sub-shell ( $() ) # https://github.com/rprichard/winpty/commit/222ecb9f4404cce3cdbafa0a97c7c3da4ce2b3c2 # I wish it was documented... Could have saved many hours of pain making things work on Windows. winpty='winpty -Xallow-non-tty' } is_docker_running () { if ! is_linux && ! is_docker_native && ! is_wsl; then if ! is_docker_machine_running; then return 255; fi fi # Check if docker is running via docker info. # This operation is instant even if docker is not running (assuming a socket is used). which docker >/dev/null 2>&1 && docker info >/dev/null || return 1 } is_docksal_running () { local system_services=$(docker ps --filter "label=io.docksal.group=system" --format '{{.Names}}' 2>/dev/null) # Assume Docksal is running when the following system services are running (echo "$system_services" | grep 'docksal-vhost-proxy' >/dev/null 2>&1) || return 1 (echo "$system_services" | grep 'docksal-ssh-agent' >/dev/null 2>&1) || return 1 (echo "$system_services" | grep 'docksal-dns' >/dev/null 2>&1) || return 1 return 0 } # Check whether we have a working tty. # Otherwise we are running in a non-tty environment ( e.g., Babun on Windows). # We assume the environment is interactive if there is a tty. # All other direct checks don't work well in and every environment and scripts. is_tty () { [[ "$(/usr/bin/tty || true)" != "not a tty" ]] # TODO: rewrite this check using [ -t ] test # http://stackoverflow.com/questions/911168/how-to-detect-if-my-shell-script-is-running-through-a-pipe/911213#911213 # 0: stdin, 1: stdout, 2: stderr # [ -t 0 -a -t 1 ] } # Checks whether fin is run under root privileges is_root () { [[ "$(id -u)" == "0" ]] } #---------------------------- Other helper functions ------------------------------- testing_warn () { [[ "$DOCKSAL_VERSION" != 'master' ]] && is_tty && \ echo-yellow "[!] Using Docksal version: ${DOCKSAL_VERSION}" } # Convert version string like 1.2.3 to integer for comparison # param $1 version string of 3 components max (e.g., 1.10.3) ver_to_int () { echo "$@" | awk -F. '{ printf("%d%03d%03d", $1,$2,$3); }' } #---------------------------- Control functions ------------------------------- # Check that project root exists and Docker is running. check_project_environment () { check_project_root && check_docker_running } # Check that project root exists, Docker is running and Docksal system services are running check_docksal_environment () { # Since network configuration is not permanent on Linux we need to restore it when possible # check_docksal_environment is a good place to do it, but we don't need to know the result if is_linux; then configure_network_alpine "on" configure_resolver_alpine "on" fi check_project_root && check_docksal_running } check_docker_running () { # Immediately break on upgrade [[ "$UPGRADE_IN_PROGRESS" == "1" ]] && return 0 # Check docker versions in this function if ! is_docker_version; then echo-alert "Required Docker version is $REQUIREMENTS_DOCKER" \ "Run ${yellow}fin update${NC} to update to the required version." \ "" fi # Check cached value [[ "$DOCKER_RUNNING" == "true" ]] && return 0 # If we got here then cached value is not true local docker_status check_binary_found 'docker' check_binary_found 'docker-compose' is_docker_running docker_status=$? # Last chance to return non-error code [[ ${docker_status} -eq 0 ]] && return if [[ ${docker_status} -eq 255 ]] && (! is_docker_native && ! is_wsl); then echo-yellow "It looks like '$DOCKER_MACHINE_NAME' docker machine is not running." _confirm "Run 'fin vm start' to start it now?" docker_machine_start if_failed "Could not start Docksal docker machine properly" # re-check status eval $(docker-machine env --shell=bash "$DOCKER_MACHINE_NAME") docker info >/dev/null docker_status=$? fi if is_linux; then # Remind the user about running 'newgrp docker' after install. if ! (id -nG | grep docker >/dev/null 2>&1) && ! is_root ; then echo-error "Current user is not part of the docker group." \ "Re-login to enable permanent Docker support." "Run ${yellow}newgrp docker${NC} to enable Docker support for current terminal session only." exit 1 fi # Check if docker daemon is running and offer to start it if not. if ! (ps aux | grep dockerd | grep -v grep); then _confirm "Start docker daemon now ('service docker start')?" sudo service docker start fi else # MacOS, Windows # Check for certificates error if (docker-machine env "$DOCKER_MACHINE_NAME" 2>&1 | grep "Error checking TLS connection" >/dev/null); then echo-error "Error connecting to Docker due to certificates error" \ "WHAT TO DO?" \ "1. Try ${yellow}fin vm restart${NC}." \ "2. If #1 did not help, try restarting your local host." \ "3. If #2 did not help, try ${yellow}fin vm regenerate-certs${NC}." exit 1 fi # Other generic errors if is_docker_native; then echo-error "Your Docker client is unable to talk to the Docker server." \ "WHAT TO DO?" \ "Check that your Docker for Mac/Win app is running." exit 1 else echo-error "Your Docker client is unable to talk to the Docker server." \ "WHAT TO DO?" \ "1. Try ${yellow}fin vm restart${NC}." \ "2. If #1 did not help, run ${yellow}fin update${NC} to update." \ "3. Check errors you see in troubleshooting doc https://docs.docksal.io/en/master/troubleshooting" exit 1 fi fi } check_docksal_running () { check_docker_running if ! is_docksal_running; then echo-alert "Some Docksal services were not running" "Restarting Docksal system services..." sleep 1 system_reset fi } check_macos_path () { if is_mac && [[ ! "$(pwd)" =~ "$HOME" ]]; then echo-alert "Current project is outside of your home folder!" \ "Move your project into $HOME/Projects folder" sleep 2 fi } is_vbox_version () { ! which "$vboxmanage" >/dev/null 2>&1 && return 1 local virtualbox_version=$("$vboxmanage" -v | sed "s/r.*//" 2>/dev/null) [[ $(ver_to_int "$virtualbox_version") < $(ver_to_int "$REQUIREMENTS_VBOX") ]] && \ return 2 return 0 } is_docker_version () { ! which docker >/dev/null 2>&1 && return 1 local version=$(docker version --format '{{.Client.Version}}' 2>/dev/null) [[ $(ver_to_int "$REQUIREMENTS_DOCKER") -le $(ver_to_int "$version") ]] } is_docker_server_version () { ! which docker >/dev/null 2>&1 && return 1 local version=$(docker version --format '{{.Server.Version}}' 2>/dev/null) [[ $(ver_to_int "$REQUIREMENTS_DOCKER") -le $(ver_to_int "$version") ]] } is_docker_compose_version () { ! which docker-compose >/dev/null 2>&1 && return 1 # Trim any control characters from docker-compose output (necessary on Windows) local version=$(docker-compose version --short | tr -d '[:cntrl:]') [[ $(ver_to_int "$REQUIREMENTS_DOCKER_COMPOSE") -le $(ver_to_int "$version") ]] } is_docker_machine_version () { ! which docker-machine >/dev/null 2>&1 && return 1 local version=$(docker-machine -v | sed "s/.*version \(.*\),.*/\1/") [[ $(ver_to_int "$REQUIREMENTS_DOCKER_MACHINE") -le $(ver_to_int "$version") ]] } is_winpty_version () { ! which "$WINPTY_BIN" >/dev/null 2>&1 && return 2 local version=$("$WINPTY_BIN" --version | head -1 | sed "s/.*version \(.*\).*/\1/") [[ $(ver_to_int "$REQUIREMENTS_WINPTY") -le $(ver_to_int "$version") ]] } wants_rc_version () { [[ -n "${DOCKSAL_USE_RC}" ]] } check_vbox_version () { # Provide ability to skip checking for vbox version [[ "$SKIP_VBOX_VERSION_CHECK" == "1" ]] && return; is_vbox_version local res=$? [[ "$res" == "1" ]] && echo-error "$vboxmanage binary was not found" && exit 1 [[ "$res" == "2" ]] && echo-error "VirtualBox version should be $REQUIREMENTS_VBOX or higher" && exit 1 } # Check that .docksal is present check_project_root () { if [[ "$(get_project_path)" == "" ]] ; then echo-error "Cannot detect project root." \ "Please make sure you have ${yellow}.docksal${NC} directory in the root of your project." \ "To setup a basic Docksal stack in the current directory run ${yellow}fin config generate${NC}" exit 1 fi } # Project uniqueness checks check_project_unique () { # Format: project:project-root:virtual-host local _projects=$(docker ps --all \ --filter 'label=io.docksal.project-root' \ --format '{{.Label "com.docker.compose.project"}}:{{.Label "io.docksal.project-root"}}:{{.Label "io.docksal.virtual-host"}}') for _project in $_projects; do IFS=':' read _project_name _project_root _project_vhost <<< "$_project" # Prevent duplicate project names if [[ "$_project_name" == "$COMPOSE_PROJECT_NAME_SAFE" ]] && [[ "$_project_root" != "$PROJECT_ROOT" ]]; then echo-error "Another project is already using the name '${COMPOSE_PROJECT_NAME_SAFE}'" \ "Change the name of the current project by renaming the project folder (folder name defines the project name and has to be unique)" \ "or remove the other project's stack by running ${yellow}fin project remove${NC} in ${yellow}$_project_root${NC}" exit 1 fi # Prevent duplicate vhost names # TODO: handle non-default VIRTUAL_HOST definitions if [[ "$_project_vhost" =~ ^$VIRTUAL_HOST ]] && [[ "$_project_root" != "$PROJECT_ROOT" ]]; then echo-error "Another project is already using the virtual hostname '${VIRTUAL_HOST}'" \ "Use a different hostname for this project by overriding the VIRTUAL_HOST variable in docksal.env" \ "or remove the other project's stack by running ${yellow}fin project remove${NC} in ${yellow}$_project_root${NC}" exit 1 fi done } # Yes/no confirmation dialog with an optional message # @param $1 confirmation message # @param $2 --no-exit _confirm () { # Skip checks if not running interactively (not a tty or not on Windows) if ! is_tty || is_travis; then return 0; fi while true; do echo -en "$1 " read -p "[y/n]: " answer case "$answer" in [Yy]|[Yy][Ee][Ss] ) break ;; [Nn]|[Nn][Oo] ) [[ "$2" == "--no-exit" ]] && return 1 exit 1 ;; * ) echo 'Please answer yes or no.' esac done } #-------------------------- Containers management ----------------------------- _start_containers () { check_docker_running echo-green "Starting services..." docker-compose up -d --remove-orphans --build || return 1 _healthcheck_wait _vhost_proxy_connect } # @param $1 "-a" || "--all" _stop_containers () { if [[ $1 == '-a' ]] || [[ $1 == '--all' ]]; then check_docker_running # stop all but system containers (--label "io.docksal.group=system") local containers containers=$(docker ps --format '{{.Names}} {{.Label "io.docksal.group"}}' | grep -v 'system') if [[ $? == 0 ]]; then echo-green "Stopping all running Docksal projects..." local running_projects=$(docker ps \ --filter 'label=io.docksal.project-root' \ --format '{{.Label "io.docksal.project-root"}}' ) local i=1 for project in ${running_projects}; do echo "$i. $project" # use updated version when updated (cd "$project" && fin stop) ((i=$i+1)) done else echo "No Docksal projects are running" fi return fi if [[ "$1" == "proxy" ]] ; then echo-green 'Stopping Docksal HTTP/HTTPS reverse proxy service...' docker stop docksal-vhost-proxy >/dev/null return fi if [[ "$1" == "dns" ]] ; then echo-green 'Stopping Docksal DNS service...' docker stop docksal-dns >/dev/null return fi if [[ "$1" == "ssh-agent" ]] ; then echo-green 'Stopping Docksal ssh-agent service...' docker stop docksal-ssh-agent >/dev/null return fi load_configuration check_project_unique echo-green "Stopping services..." docker-compose stop "$@" } _restart_containers () { _stop_containers "$@" && sleep 1 && _start_containers } # @param $1 container_name _remove_containers () { check_project_environment if [[ $1 == "" ]]; then echo-yellow "Removing containers..." # Disconnect proxy from the project network, otherwise network will not be removed. # Figure out the default project network name local network="${COMPOSE_PROJECT_NAME_SAFE}_default" docker network disconnect "$network" docksal-vhost-proxy >/dev/null 2>&1 # Taking the whole docker-compose project down (this removes containers, volumes and networks) docker-compose down --volumes --remove-orphans else # Removing requested containers only docker-compose kill "$@" && docker-compose rm -vf "$@" fi } # Cleanup unused resources # @param $1 --hard if set removes all stopped containers cleanup () { check_docker_running if [[ "$1" == "--hard" ]] && [[ "$(docker ps -aqf status=exited)" != "" ]]; then echo -e "${red}WARNING: ${yellow}Preparing to delete the following stopped containers:${NC}" docker ps -af status=exited --format "{{.Names}}\t{{.Status}}\t{{.Image}}" printf '–%.0s' $(seq 1 40) echo -e "${yellow}" _confirm "Continue?" echo -e "${NC}" #-- echo-green "Removing stopped containers..." docker container prune -f 2>/dev/null fi echo-green "Checking for orphaned projects..." local projects=$(docker ps --all \ --filter 'label=io.docksal.project-root' \ --format '{{ .Label "com.docker.compose.project" }}:{{ .Label "io.docksal.project-root" }}') for project in $projects; do IFS=':' read project_name project_root <<< "$project" if [[ ! -d "$project_root" ]]; then echo "Directory for project \"$project_name\" does not exist. Removing containers..." docker ps -qa --filter "label=com.docker.compose.project=${project_name}" | xargs docker rm -f fi done echo-green "Removing dangling images..." docker image prune -f 2>/dev/null echo-green "Removing dangling volumes..." docker volume prune -f 2>/dev/null # TODO: this is temporary disabled as it causes issues. See https://github.com/docksal/docksal/issues/582 # echo-green "Removing dangling networks..." # install_proxy_service # Reset vhost-proxy to disconnect it from abandoned networks # docker network prune -f 2>/dev/null } # Connect vhost-proxy to all bridge networks on the host _vhost_proxy_connect () { # Figure out the default project network name local network="${COMPOSE_PROJECT_NAME_SAFE}_default" docker network connect "$network" docksal-vhost-proxy >/dev/null 2>&1 if [[ $? == 0 ]]; then echo-green "Connected vhost-proxy to \"${network}\" network." # Run a dummy container to trigger docker-gen to refresh proxy configuration. docker run --rm --entrypoint=echo ${IMAGE_VHOST_PROXY} >/dev/null 2>&1 fi } # Checks container health status (if available) # Relies on healthcheck introduced in docksal/cli v1.3.0+, uses `sleep` as a fallback # @param $1 container id/name _healthcheck () { local health_status health_status=$(docker inspect --format='{{json .State.Health.Status}}' "$1" 2>/dev/null) # Wait for 5s then exit with 0 if a container does not have a health status property # Necessary for backward compatibility with images that do not support health checks if [[ $? != 0 ]]; then echo "Waiting 10s for container to start..." sleep 10 return 0 fi # If it does, check the status echo $health_status | grep '"healthy"' >/dev/null 2>&1 } # Waits for containers to become healthy # For reasoning why we are not using `depends_on` `condition` see here: # https://github.com/docksal/docksal/issues/225#issuecomment-306604063 # TODO: make this universal. Currently hardcoded for cli only. _healthcheck_wait () { # Wait for cli to become ready by watching its health status local container_name="${COMPOSE_PROJECT_NAME_SAFE}_cli_1" local delay=5 local timeout=60 local elapsed=0 until _healthcheck "$container_name"; do echo "Waiting for $container_name to become ready..." sleep "$delay"; # Give the container 30s to become ready elapsed=$((elapsed + delay)) if ((elapsed > timeout)); then echo-error "$container_name heathcheck failed" \ "Container did not enter a healthy state within the expected amount of time." \ "Try ${yellow}fin project restart${NC}" exit 1 fi done return 0 } #------------------------------ Help functions -------------------------------- # Nicely prints command help # @param $1 command name # @param $2 description # @param $3 [optional] command color printh () { local COMMAND_COLUMN_WIDTH=25; case "$3" in yellow) printf " ${yellow}%-${COMMAND_COLUMN_WIDTH}s${NC}" "$1" echo -e " $2" ;; green) printf " ${green}%-${COMMAND_COLUMN_WIDTH}s${NC}" "$1" echo -e " $2" ;; *) printf " %-${COMMAND_COLUMN_WIDTH}s" "$1" echo -e " $2" ;; esac } # Show help for fin or for certain command # $1 name of command to show help for show_help () { local project_commands_path="$(get_project_path)/$DOCKSAL_COMMANDS_PATH" local global_commands_path="$HOME/$DOCKSAL_COMMANDS_PATH" local custom_commands_list # If nonempty param then show help for a certain command if [[ ! -z "$1" ]]; then # Check for help function for specific command type "show_help_$1" >/dev/null 2>&1 if [ $? -eq 0 ]; then show_help_$1 exit fi # Search for addons addon="$(get_addon_script $1)" # If no such addon then search for command [[ ! -f "$addon" ]] && addon="$(get_command_script $1)" if [ -f "$addon" ]; then echo -en "${green}fin $1${NC} - " local _help_contents=$(cat "$addon" | grep '^##' | sed -E "s/^##[ ]?//g") echo -e "$_help_contents" echo exit fi fi echo "Docksal control cli utility v$FIN_VERSION" echo echo "Usage: fin " echo echo-green "Management Commands:" printh "db " "Manage databases (${yellow}fin help db${NC})" "yellow" printh "project " "Manage project(s) (${yellow}fin help project${NC})" "yellow" printh "system " "Manage Docksal (${yellow}fin help system${NC})" "yellow" if ! is_linux && ! is_docker_native; then printh "vm " "Manage Docksal VM (${yellow}fin help vm${NC})" "yellow" fi echo echo-green "Commands:" printh "bash [service]" "Open shell into service's container. Defaults to ${yellow}cli${NC}" printh "logs [service]" "Show service logs (e.g., Apache logs, MySQL logs) and Unison logs (${yellow}fin help logs${NC})" printh "exec " "Execute a command or a script in ${yellow}cli${NC}" printh "config [command]" "Show or change configuration (${yellow}fin help config${NC})" printh "run-cli (rc) " "Run a command in a standalone cli container in the current directory (${yellow}fin help run-cli${NC})" echo printh "drush [command]" "Drush command (requires Drupal)" printh "drupal [command]" "Drupal Console command (requires Drupal 8)" printh "platform [command]" "Platform.sh's CLI (requires docksal/cli 2.3+)" printh "terminus [command]" "Pantheon's Terminus (requires docksal/cli 2.1+)" printh "wp [command]" "WordPress CLI command (requires WordPress)" printh "composer [command]" "Run Composer commands" printh "docker [command]" "Run Docker commands directly" printh "docker-compose [command]" "Run docker-compose commands directly" echo printh "init" "Initialize a project (override it with your own automation ${yellow}fin help init${NC})" printh "addon " "Addons management commands: install, remove (${yellow}fin help addon${NC})" printh "ssh-add [-lD] [key]" "Adds ssh private key to the authentication agent (${yellow}fin help ssh-add${NC})" printh "alias" "Manage aliases that allow ${yellow}fin @alias${NC} execution (${yellow}fin help alias${NC})" printh "cleanup [--hard]" "Remove unused Docker images and projects (saves disk space)" printh "share" "Create temporary public url for current project using ngrok" printh "exec-url " "Download script from URL and run it on host (URL should be public)" printh "image " "Image management commands: registry, save, load (${yellow}fin help image${NC})" printh "hosts " "Hosts file commands: add, remove, list (${yellow}fin help hosts${NC})" printh "vhosts" "List all virtual *.docksal hosts registered in Docksal proxy" printh "sysinfo" "Show system information" printh "diagnose" "Show diagnostic information for troubleshooting and bug reporting" printh "version (--version, v, -v)" "Print fin version. [v, -v] prints short version" printh "update" "${yellow}Update Docksal${NC}" "yellow" # Show list of custom commands and their help if available load_global_configuration if ! empty_dir "$PROJECT_ROOT/$DOCKSAL_ADDONS_PATH" || ! empty_dir "$HOME/$DOCKSAL_ADDONS_PATH"; then echo echo-green "Addons:" fi if [[ ! -z "$(get_project_path)" ]]; then show_help_list_addons_in_help 'project' fi if [[ ! -z "$HOME/$DOCKSAL_COMMANDS_PATH" ]]; then show_help_list_addons_in_help 'global' fi # Show list of custom commands and their help if available if ! empty_dir "$PROJECT_ROOT/$DOCKSAL_COMMANDS_PATH" || ! empty_dir "$HOME/$DOCKSAL_COMMANDS_PATH"; then echo echo-green "Custom commands:" fi if [[ ! -z "$(get_project_path)" ]]; then show_help_list_commands_in_help 'project' fi if [[ ! -z "$HOME/$DOCKSAL_COMMANDS_PATH" ]]; then show_help_list_commands_in_help 'global' fi echo } # param $1 - 'project' or 'global' show_help_list_commands_in_help () { local addons_path if [[ "$1" == 'project' ]]; then addons_path="$(get_project_path)/$DOCKSAL_COMMANDS_PATH" # avoid taking global .docksal folder for project one [[ "$addons_path" == "$HOME/$DOCKSAL_COMMANDS_PATH" ]] && return else addons_path="$HOME/$DOCKSAL_COMMANDS_PATH" fi local executable="-executable" [[ is_mac ]] && executable="-perm +100" local addons_list=$(find "$addons_path" -type f ${executable} 2>/dev/null | sort -n 2>/dev/null) if [[ "$addons_list" != "" ]]; then local scope for cmd_name in ${addons_list} do local g [[ "$1" == 'global' ]] && g=' [g]' cmd_name=${cmd_name/${addons_path}\//} # command description is lines that start with ## local filename="$addons_path/$cmd_name" local cmd_desc [[ ! -f "$filename" ]] && continue cmd_desc=$(cat "$filename" | grep '^##' | sed "s/^##[ ]*//g" | head -1 --) cmd_desc="${cmd_desc:-No description}" printh "${cmd_name}${g}" "$cmd_desc" done fi } # param $1 - 'project' or 'global' show_help_list_addons_in_help () { local addons_path if [[ "$1" == 'project' ]]; then addons_path="$(get_project_path)/$DOCKSAL_ADDONS_PATH" # avoid taking global .docksal folder for project one [[ "$addons_path" == "$HOME/$DOCKSAL_ADDONS_PATH" ]] && return else addons_path="$HOME/$DOCKSAL_ADDONS_PATH" fi local addons_list=$(ls "$addons_path" 2>/dev/null | sort -n 2>/dev/null | tr "\n" " ") if [[ ! -z "$addons_list" ]]; then local scope for cmd_name in $(ls "$addons_path") do local g [[ "$1" == 'global' ]] && g=' [g]' # command description is lines that start with ## local filename="$addons_path/$cmd_name/$cmd_name" local cmd_desc [[ ! -f "$filename" ]] && continue cmd_desc=$(cat "$filename" | grep '^##' | sed "s/^##[ ]*//g" | head -1 --) cmd_desc="${cmd_desc:-No description}" printh "${cmd_name}${g}" "$cmd_desc" done fi } show_help_ssh-add () { echo echo "Add private key identities stored in \$HOME/.ssh to the docksal/ssh-agent." echo "When run without arguments, automatically adds the default key files (id_rsa, id_dsa, id_ecdsa)." echo "A custom key name can be given as an argument: fin ssh-add [keyname]." echo "[keyname] is the file name within \$HOME/.ssh" echo echo "Usage: ssh-add [-lD] [key]" echo echo "Options:" printh "-D" "Deletes all keys from the agent." printh "-l" "Lists all keys currently loaded by the agent." echo echo "Examples:" printh "fin ssh-add my_custom_key" "Add \$HOME/.ssh/my_custom_key to SSH Agent." } show_help_exec () { echo echo "Execute command or file in \`cli\` service container." echo "fin exec will automatically cd into the same folder inside \`cli\`." echo echo "Usage: exec [options] " echo echo "Options:" printh "-T" "Disable pseudo-tty allocation." printh "" "Useful for non-interactive commands when output is saved into a variable for further comparison." printh "" "In a TTY mode the output may contain unexpected invisible control symbols." printh "--in=[name]" "Name of the service to execute the command in." echo echo "Examples:" printh "fin exec ls -la" " Current directory listing" printh "fin exec \"ls -la > /tmp/list\"" " Execute advanced shell command with pipes or stdout redirects happening inside \`cli\`" printh "res=\$(fin exec -T drush st)" " Use -T switch when using exec output" printh "fin exec .docksal/script.sh" " Execute a whole file inside \`cli\` container" printh "fin exec --in=db mysql -uroot -p" " Execute command in \`db\` container (will NOT cd into the same folder)" } show_help_run-cli () { echo echo "Runs commands in a standalone \`cli\` container mapped to the current directory." echo "Container has a persistent \$HOME directory where something can be saved in between launches." echo "NOTE: \`fin cleanup\` will clean the persistent \$HOME directory" echo echo "Usage: run-cli [options] " echo " rc [options] " echo echo "Options:" printh "--clean" "Run command with a non-persistent \$HOME directory" printh "--cleanup" "Clean the persistent \$HOME directory and run command" printh "--debug" "Print container debug output" printh "--image=IMAGE" "Override default container image" printh "-e VAR=VALUE" "Pass environment variable(s) to the container" printh "-T" "Disable pseudo-tty allocation (useful to get clean stdout)" echo echo "Examples:" printh "fin rc ls -la" " Current directory listing" printh "fin rc \"ls -la > /tmp/list\"" " Execute advanced shell command with pipes or stdout redirects happening inside cli" printh "fin rc -e VAR1=hello -e VAR2=world 'echo \$VAR1 \$VAR2'" "Print hello world using ENV variables" } show_help_image() { echo echo "Docksal images listing and saving" echo echo "Usage: image " echo echo "Commands:" printh "registry" "Show all Docksal images on Docker Hub" printh "registry [image name]" "Show all tags for a certain image" printh "save --system,--project,--all" "Save docker images into a tar archive." printh "load " "Load docker images from a tar archive." echo echo "Examples:" printh "fin image registry" "Show all available Docksal images on Docker Hub" printh "fin image registry docksal/db" "Show all tags for docksal/db image" printh "fin image save --system" "Save Docksal system images." printh "fin image save --project" "Save current project's images." printh "fin image save --all" "Save all images available on the host." } show_help_project() { echo echo "Project management" echo echo "Usage: project [params]" echo echo "Commands:" printh "start" "Start project services (alias: fin start)" printh "up" "Configuration re-read and start project services (alias: fin up)" printh "stop [option] [service]" "Stop all or specified project services (alias: fin stop)" printh " --all (-a)" "Stop all services on all Docksal projects" echo printh "status" "List project services (alias: fin ps)" printh "restart" "Restart project services (alias: fin restart)" printh "reset [service]" "Recreate all or specified project services, their containers and named volumes" printh "" "Changes to home directory in \`cli\` are preserved." echo printh "remove [option] [service]" "Remove all project services, networks and all their volumes, or specified services only" printh " rm [option] [service]" "" printh " --force (-f)" "Do not ask for confirmation when deleting all project services" echo printh "list [option]" "List running Docksal projects (alias: fin pl)" printh " --all (-a)" "List all Docksal projects (stopped as well)" echo printh "create [options]" "Create a new project with a pre-configured boilerplate:" printh "" "Drupal, Wordpress, Magento, Laravel, Backdrop, Hugo, Gatsby, and others" printh " --name=name" "Provide project name upfront" printh " --choice=#" "Provide software choice number upfront" printh " --yes (-y)" "Avoid confirmation" echo printh "config" "Show project configuration" printh "build" "Build or rebuild services (alias for 'docker-compose build')" echo echo "Examples:" printh "fin pl -a" "List all known Docksal projects, including stopped ones" printh "fin project reset db" "Reset only DB service to start with DB from scratch" printh "fin project create" "Start a new project wizard" } show_help_reset () { show_help_project } show_help_rm () { show_help_project } show_help_remove () { show_help_project } show_help_system() { echo echo "Manage Docksal system status (Docker should be running)" echo echo "Usage: system [params]" echo echo "Commands:" printh "reset" "Reset Docksal" printh "start" "Start Docksal" printh "stop" "Stop Docksal" printh "status" "Check Docksal status" echo echo "Examples:" printh "fin system reset" "Reset all Docksal system services and settings" printh "fin system reset dns" "Reset Docksal DNS service" printh "fin system reset vhost-proxy" "Reset Docksal HTTP/HTTPS reverse proxy service (resolves ${yellow}*.docksal${NC} domain names into container IPs)" printh "fin system reset ssh-agent" "Reset Docksal ssh-agent service" } show_help_update () { echo echo "Update Docksal system components to the latest stable version" echo echo "Usage: update" echo echo "Options:" printh "DOCKSAL_VERSION=develop fin update" "Update Docksal to the latest development version" } show_help_exec-url () { echo echo "Fetch script from the public URL and evaluate locally" echo echo "Usage: fin exec-url " } show_help_sqlc () { echo echo "Open MySql command line to the current db server" echo echo "Usage: sqlc [options]" echo " mysql [options]" echo echo "Options:" printh "--db-user=admin" "Use another mysql username (default is 'root')" printh "--db-password=p4\$\$" "Use another database password (default is the one set with 'MYSQL_ROOT_PASSWORD', see ${yellow}fin config${NC})" } show_help_mysql () { show_help_sqlc } show_help_sqls () { echo echo-green "Show list of available databases (alias: )" echo echo "Usage: sqls [options]" echo " mysql-list [options]" echo echo-green "Options:" printh "--db-user=admin" "Use another mysql username (default is 'root')" printh "--db-password=p4\$\$" "Use another database password (default is the one set with 'MYSQL_ROOT_PASSWORD', see ${yellow}fin config${NC})" } show_help_mysql_list () { show_help_sqls } show_help_sqli () { echo echo "Truncate database and import SQL dump from a file or stdin" echo echo "Usage: fin sqli [dump_file.sql] [options]" echo " fin mysql-import [dump_file.sql] [options]" echo echo "Options:" printh "--force" "Do not ask questions. The only time when question is asked is when truncation fails." printh "--db=drupal" "Use another database (default is the one set with 'MYSQL_DATABASE')" printh "--db-user=admin" "Use another mysql username (default is 'root')" printh "--db-password=p4\$\$" "Use another database password (default is the one set with 'MYSQL_ROOT_PASSWORD', see ${yellow}fin config${NC})" echo echo "Examples:" printh "fin sqli ~/dump.sql --db=drupal" "Import plaintext sql dump into database named 'drupal' (DB should exist)" printh "cat ~/dump.sql | fin sqli" " Import dump from stdin into default database" } show_help_mysql-import () { show_help_sqli } show_help_sqld () { echo echo "Export database from db container into file or stdout (alias: mysql-dump)" echo echo "Usage: sqld [dump_file] [options]" echo echo "Options:" printh "--db=drupal" "Use another database (default is the one set with 'MYSQL_DATABASE')" printh "--db-user=admin" "Use another mysql username (default is 'root')" printh "--db-password=p4\$\$" "Use another database password (default is the one set with 'MYSQL_ROOT_PASSWORD', see ${yellow}fin config${NC})" echo echo "Examples:" printh "fin sqld ~/dump.sql" "Export default database dump" printh "fin sqld --db=drupal" "Export database 'drupal' dump into stdout" } show_help_mysql-dump () { show_help_sqld } show_help_db () { echo echo "Database management commands" echo echo "Usage: db [file] [options]" echo echo "Commands:" printh "import [file] [options]" "Truncate the database and import from SQL dump file or stdin." printh " --progress" "Show import progess (requires pv)." printh " --no-truncate" "Do no truncate database before import." printh "dump [file]" "Dump a database into an SQL dump file or stdout." printh "list (ls)" "Show list of existing databases." printh "cli [query]" "Open command line interface to the DB server (and execute query if provided)." printh "create " "Create a database." printh "drop " "Delete a database." printh "truncate [name]" "Truncate a database (defaults to the \`default\`)" echo echo "Options:" printh "--db=drupal" "Use another database (default is the one set with 'MYSQL_DATABASE')" printh "--db-user=admin" "Use another mysql username (default is 'root')" printh "--db-password=p4\$\$" "Use another database password (default is the one set with 'MYSQL_ROOT_PASSWORD', see ${yellow}fin config${NC})" printh "--db-charset=utf8" "Override charset when creating a database (default is utf8)" printh "--db-collation=utf8mb4" "Override collation when creating a database (default is utf8_general_ci)" echo echo "Examples:" printh "fin db import ~/dump.sql" " Import from dump.sql file" printh "fin db import ~/dump.sql --progress" " Import from dump.sql file showing import progress" printh "fin db import ~/partial.sql --no-truncate" "Import partial.sql without truncating DB" echo printh "cat dump.sql | fin db import" " Import dump from stdin into default database" printh "zcat < dump.sql.gz | fin db import" " Import archived dump from stdin into default database" printh "fin db dump ~/dump.sql" " Export default database into dump.sql" printh "fin db dump --db=drupal" " Export database 'drupal' dump into stdout" printh "fin db dump --db=mysql --db-user=root --db-password=root mysql.sql Export mysql database as root into mysql.sql" echo printh "fin db cli --db=nondefault 'select * from users' Execute query on database other than MYSQL_DATABASE" printh "fin db create project2 --db-charset=utf8mb4 Create database project2 with utf8mb4 charset" } show_help_vm () { echo echo "Control Docksal virtual machine" echo echo "Usage: vm " echo echo "Commands:" printh "start" "Start the machine (create if needed)" printh "stop" "Stop the machine" printh "kill" "Forcibely stop the machine" printh "restart" "Restart the machine" printh "status" "Get the status" printh "ssh [command]" "Log into ssh or run a command via ssh" printh "remove" "Remove Docksal machine and cleanup after it" printh "ip" "Show the machine IP address" # printh "ip [new ip]" "Set machine IP address (requires vm restart)" printh "ls" "List all docker machines" printh "env" "Display the commands to set up the shell for direct use of Docker client" printh "mount" "Try remounting host filesystem (NFS on macOS, SMB on Windows)" echo printh "ram" "Show memory size" printh "ram [megabytes]" "Set memory size. Default is 1024 (requires vm restart)" printh "hdd" "Show disk size and usage" printh "stats" "Show CPU and network usage" echo printh "regenerate-certs" "Regenerate TLS certificates and restart the VM" } show_help_alias () { echo echo "Create, update, or delete project aliases." echo "Aliases provide functionality that is similar to drush aliases." echo "With alias you are able to execute a command in a project without navigating to the project folder." echo "You can precede any command with an alias." echo "Aliases are effectively symlinks stored in \$HOME/.docksal/alias" echo echo "Usage: alias [alias_name]" echo echo "Commands:" printh "list" "Show aliases list" printh " " "Create/update alias with that links to " printh "remove " "Remove alias" echo echo "Examples:" printh "fin alias ~/site1/docroot dev" " Create or update an alias ${yellow}dev${NC} that is linked to ${yellow}~/site1/docroot${NC}" printh "fin @project1 drush st" " Execute \`drush st\` command in directory linked by ${yellow}project1${NC} alias" printh "" " Hint: create alias linking to Drupal sub-site to launch targeted commands" printh "fin alias remove project1" " Delete ${yellow}project1${NC} alias" } show_help_share () { echo echo "Share the project via public url generated with ngrok" echo echo "Usage: share [options]" echo echo "Options:" printh "--host" "Override a hostname for ngrok to route to (default is \$VIRTUAL_HOST)" echo echo "Usage:" printh "You will get public web address in the ngrok command line UI." printh "Press Ctrl+C to stop sharing and quit command line UI" echo echo "Examples:" printh "fin share --host=subdomain.mysite.docksal" "Expose certain subdomain" } show_help_config () { echo echo "Display, generate, or change project configuration" echo echo "Usage: config [command]" echo echo "Commands:" printh "show [options]" "Display configuration for the current project" printh " --show-secrets" "Do not truncate value of SECRET_* environment vars" printh "env" "Display only environment variables section" printh "generate [options]" "Generate empty Docksal configuration for the project" printh " --stack=acquia" "Set non-default DOCKSAL_STACK during config generate" printh " --docroot=mydir" "Set non-default DOCROOT during config generate" printh "set [options] [VAR=VAL]" "Set value(s) for the variable(s) in project ENV file" printh " --global" "Set for global ENV file" printh " --env=[name]" "Set in environment specific project ENV file (default: local)" echo printh "remove [options] [VAR]" "Remove variable(s) from project ENV file" printh "rm [options] [VAR]" "" printh " --global" "Remove from global ENV file" printh " --env=[name]" "Remove from environment specific project ENV file (default: local)" echo printh "get [options] [VAR]" "Get the value of the single variable from project ENV file" printh " --global" "Get value from global ENV file" printh " --env=[name]" "Get value from environment specific project ENV file (default: local)" echo echo "Examples:" printh "fin config set DOCKER_NATIVE=1 --global" "Adds DOCKER_NATIVE=1 into \$HOME/.docksal/docksal.env" printh "fin config rm DOCKER_NATIVE --global" " Removes DOCKER_NATIVE value from \$HOME/.docksal/docksal.env" } show_help_init () { echo echo "Creates default project configuration and starts a project, when no project found." echo "This built-in is meant to be overridden with your custom command or addon." echo -e "See ${yellow}https://docs.docksal.io/en/master/fin/custom-commands${NC} on docs for creating custom commands." echo echo "Usage: init" echo echo "Examples:" printh "- https://github.com/docksal/drupal8/tree/master/.docksal/commands" " Automation example" printh "- https://github.com/docksal/example-gatsby/tree/master/.docksal/commands" "Automation example" printh "- https://github.com/docksal/addons" " Addon repo" } show_help_cleanup () { echo echo "Remove unused docker images and orphaned containers to save disk space." echo "Orphaned are containers which folders were deleted from the filesystem," echo "but their containers still linger in the Docker." echo echo "Usage: cleanup [options]" echo echo "Options:" printh "--hard" "Also remove ALL stopped containers (potentially destructive operation)" } show_help_hosts () { echo echo "Add or remove lines to/from OS-dependent hosts file (e.g., /etc/hosts)" echo echo "Usage: hosts [command]" echo echo "Commands:" printh "add [hostname]" "Add hostname to hosts file. If none provided uses VIRTUAL_HOST" printh "remove [hostname]" "Remove lines containing hostname from hosts file. If none provided uses VIRTUAL_HOST" printh "list" "Output hosts file" echo echo "Examples:" printh "fin hosts add" "Append current project's VIRTUAL_HOST to hosts file" printh "fin hosts add demo.docksal" "Append a line '$DOCKSAL_IP demo.docksal' to hosts file" printh "fin hosts remove" "Remove current project's VIRTUAL_HOST from hosts file" printh "fin hosts remove demo.docksal" "Remove *all* lines containing demo.docksal from hosts file" printh "fin hosts" "Output hosts file" } show_help_addon () { echo echo "Docksal Addons management commands." echo -e "See available addons in the Addons Repository ${yellow}https://github.com/docksal/addons${NC}" echo echo "Usage: addon " echo echo "Commands:" printh "install " "Install addon" printh "remove " "Remove addon" echo echo "Examples:" printh "fin addon install solr" "Install solr addon to the current project" printh "fin addon remove solr" "Uninstall solr addon from the current project" } show_help_logs () { # Display docker-compose logs --help then examples using fin echo docker-compose logs --help echo echo "Examples:" printh "fin logs web" "Show web container logs" printh "fin logs -f web" "Show web container logs and follow it" } #---------- END HELP FUNCTIONS # Display fin version # @option --short - Display only the version number version () { if [[ $1 == '--short' ]]; then echo "$FIN_VERSION" else echo "fin version: $FIN_VERSION" fi # Ping stats server stats_ping } # return bash completion words # @param $1 command to return words for bash_comp_words () { case $1 in vm) echo "start restart status stop ssh stats kill remove ip ram" exit 0 ;; alias) echo "list remove" exit 0 ;; config) echo "generate show env" exit 0 ;; db) echo "import dump list cli" exit 0 ;; project|p) echo "start stop status restart reset remove create" exit 0 ;; logs) echo "web unison" exit 0 ;; fin) local aliases=$(ls -l "$CONFIG_ALIASES" 2>/dev/null | grep -v total | awk '{printf "@%s ", $9}') local projects=$(docker ps --all \ --filter 'label=io.docksal.project-root' \ --format '@{{.Label "com.docker.compose.project"}}' | xargs ) local project_commands_path="$(get_project_path)/$DOCKSAL_COMMANDS_PATH" local project_addons_path="$(get_project_path)/$DOCKSAL_ADDONS_PATH" local global_commands_path="$HOME/$DOCKSAL_COMMANDS_PATH" local global_addons_path="$HOME/$DOCKSAL_ADDONS_PATH" local custom_commands for cmd_name in $(ls "$project_commands_path" 2>/dev/null | tr "\n" " "); do custom_commands+=" $cmd_name" done for cmd_name in $(ls "$project_addons_path" 2>/dev/null); do [[ -f "$project_addons_path/$cmd_name/$cmd_name" ]] && custom_commands+=" $cmd_name" done for cmd_name in $(ls "$global_commands_path" 2>/dev/null | tr "\n" " "); do custom_commands+=" $cmd_name" done for cmd_name in $(ls "$global_addons_path" 2>/dev/null); do [[ -f "$global_addons_path/$cmd_name/$cmd_name" ]] && custom_commands+=" $cmd_name" done echo "$aliases $projects vm start stop restart status reset remove bash exec config sqlc mysql sqli mysql-import sqld mysql-dump sqld drush drupal \ ssh-add update version cleanup sysinfo logs project $custom_commands" exit 0 ;; *) exit 1 #return 1 to completion function to prevent completion if we don't know what to do esac } #------------------------------- Docker-Machine ----------------------------------- is_docker_machine_running () { [[ "$DOCKER_MACHINE_STATUS" == 'Running' ]] } # Create docker machine # param $1 machine name docker_machine_create () { check_vbox_version check_binary_found 'docker-machine' local machine_name="${1:-$DEFAULT_MACHINE_NAME}" if is_docker_machine_exist "$machine_name"; then echo-error "Docker Machine '$machine_name' already exists" return 1 fi echo-green "Creating docker machine '$machine_name'..." # Use a local boot2docker.iso copy when available ISO_FILE=${ISO_FILE:-boot2docker.iso} if [[ -f "$ISO_FILE" ]]; then echo "Found $ISO_FILE. Using it..." URL_BOOT2DOCKER="$ISO_FILE" fi docker-machine create \ --driver=virtualbox \ --virtualbox-boot2docker-url "$URL_BOOT2DOCKER" \ --virtualbox-disk-size "${VBOX_HDD:-$DEFAULT_MACHINE_VBOX_HDD}" \ --virtualbox-memory "$DEFAULT_MACHINE_VBOX_RAM" \ --virtualbox-hostonly-cidr "$DOCKSAL_SUBNET" \ --virtualbox-no-share \ "$machine_name" if [[ $? -eq 0 ]]; then DOCKER_MACHINE_NAME="$machine_name" DOCKER_MACHINE_STATUS='Running' vm active "$machine_name" else return 1 fi } # Param $1 machine name (defaults to $DOCKER_MACHINE_NAME) # Param $2 refresh machine status true|false (defaults to false) is_docker_machine_exist () { local refresh="${2:-false}" ! which 'docker-machine' >/dev/null 2>&1 && return 1 if [[ "$1" != "" ]] && [[ "$1" != "$DOCKER_MACHINE_NAME" ]]; then docker-machine ls | grep "$1" >/dev/null 2>&1 else if [[ "$refresh" == "true" ]]; then # Refresh VM status in case in changed while the script was running. DOCKER_MACHINE_STATUS=$(docker-machine status "$DOCKER_MACHINE_NAME" 2>&1 || echo '') fi [[ "$DOCKER_MACHINE_STATUS" == *"not exist"* ]] && return 1 || return 0 fi } docker_machine_env () { local _env local res #remove cache rm -f "$CONFIG_MACHINES_ENV" 2>/dev/null # get the env _env=$(docker-machine env --shell=bash "$DOCKER_MACHINE_NAME") res=$? # apply the env eval ${_env} # return if there was an error if [[ "$res" != "0" ]]; then return ${res} fi # Save to file for reuse during subsequent fin runs to avoid running env which is expensive echo "${_env}" | tee "$CONFIG_MACHINES_ENV" >/dev/null } # Stop docker machine docker_machine_stop () { check_binary_found 'docker-machine' docker-machine stop "$DOCKER_MACHINE_NAME" } docker_machine_change_ip () { echo "(docksal) Applying IP address $DOCKER_MACHINE_IP" # extract first three parts of IP and append 255 to get broadcast mask local BROADCAST_MASK=`expr "$DOCKER_MACHINE_IP" : '\([0-9][0-9]*[0-9]*\.[0-9][0-9]*[0-9]*\.[0-9][0-9]*[0-9]*\.\)'`'255'; docker-machine ssh "$DOCKER_MACHINE_NAME" "sudo cat /var/run/udhcpc.eth1.pid | xargs sudo kill" && \ docker-machine ssh "$DOCKER_MACHINE_NAME" "sudo ifconfig eth1 $DOCKER_MACHINE_IP netmask 255.255.255.0 broadcast $BROADCAST_MASK up" && \ sleep 2 && \ docker-machine regenerate-certs "$DOCKER_MACHINE_NAME" -f sleep 2 local i=0 local _logs="" for i in `seq 20`; do _logs=$(docker-machine env "$DOCKER_MACHINE_NAME" 2>&1) if [[ $? -eq 0 ]]; then return; fi echo "IP validation (attempt #${i})..." sleep 3 done echo "$_logs" return 1 } docker_machine_start () { check_binary_found 'docker-machine' if is_docker_machine_exist; then docker-machine start "$DOCKER_MACHINE_NAME" if_failed_error "Failed starting virtual machine" DOCKER_MACHINE_STATUS='Running' # Disable IP change for now #docker_machine_change_ip && docker_machine_env if_failed_error "Failed starting virtual machine" \ "In case you see certificates problem, reboot your local host." \ "Common issues: https://docs.docksal.io/en/master/troubleshooting" configure_resolver docker_machine_mounts else # only auto-create default machine if [[ "$DOCKER_MACHINE_NAME" != "$DEFAULT_MACHINE_NAME" ]]; then echo-error "Machine '$DOCKER_MACHINE_NAME' does not exist" echo-yellow "Removing reference to non-existent machine... Run your command again." rm -f "$CONFIG_MACHINE_ACTIVE" exit 1 fi docker_machine_create && # Disable IP change for now #docker_machine_change_ip && docker_machine_env [[ $? != 0 ]] && _docker_machine_start_exception echo-green "Pulling Docksal system images..." update_system_images docker_machine_mounts fi # Catch the return code from docker_machine_mounts # TODO: figure out a more reliable implementation, as this may break if the code above gets reordered local _err=$? if [[ ${_err} -eq 0 ]]; then echo-green "Importing ssh keys..." ssh_add fi return ${_err} } # Displays an error message and offers to remove the vm if something failed _docker_machine_start_exception () { echo-error "Proper creation of virtual machine has failed" \ "For details please refer to the log above." \ "${yellow}It is recommended to remove malfunctioned virtual machine.${NC}" docker_machine_remove exit 1 } # param $1 machine name (defaults to $DOCKER_MACHINE_NAME) docker_machine_remove () { local machine_name="${1:-$DOCKER_MACHINE_NAME}" _confirm "Remove $machine_name?" # Store host-only interface name before removing the VM. local vboxifname=$("$vboxmanage" showvminfo "$machine_name" 2>/dev/null | grep "Host-only" | sed "s/.*Host-only.*'\(.*\)'.*/\1/g") docker-machine rm -f "$machine_name" if ! is_docker_machine_exist "$machine_name" "true"; then # If the VM was removed, remove the DHCP server associated with its network interface. # This ensures that we always get the lower bound IP address from the DHCP address pool (i.e., ".100"). "$vboxmanage" dhcpserver remove --netname "HostInterfaceNetworking-${vboxifname}" >/dev/null 2>&1 # Killing the network interface also helps to avoid issues when VM is recreated. "$vboxmanage" hostonlyif remove "$vboxifname" >/dev/null 2>&1 fi if is_windows; then echo-green "Removing SMB shares..." docker_machine_remove_smb fi if is_mac; then echo-green "Removing NFS exports..." docker_machine_remove_nfs fi } # Mount folder inside docker-machine with NFS # Unnamed params - shares in format "$LOCAL_FOLDER:$MOUNT_POINT_NAME" # Named --no-export param(s) - will try to mount those mount points without exporting $LOCAL_FOLDER docker_machine_mount_nfs () { local machine_name="$DOCKER_MACHINE_NAME" local network_name local nfs_ip eval $(parse_params "$@") # Get required IP addresses network_name=$("$vboxmanage" showvminfo "$machine_name" --machinereadable | grep hostonlyadapter | cut -d \" -f2) if [[ "$network_name" == "" ]]; then echo-error "Could not find virtualbox net name." && exit 1 fi # nfs_ip is an internal IP of localhost as docker machine sees it. nfs_ip=$("$vboxmanage" list hostonlyifs | grep "$network_name" -A 3 | grep IPAddress | cut -d ':' -f2 | xargs) if [[ "$nfs_ip" == "" ]]; then echo-error "Could not find virtualbox internal net IP address." && exit 1 fi machine_ip=$(docker-machine ip "$machine_name") # Remove our own old exports local exports_open="# /dev/null | \ tr "\n" "\r" | \ sed "s/${exports_open}.*${exports_close}//" | \ tr "\r" "\n" ) # Prepare new exports file exports="${exports}\n${exports_open}\n" for share in ${ARGV[@]}; do local share_export=(${share//:/ }); # Check for NFS exports conflicts manually if [[ "$exports" =~ "$share_export" ]]; then echo echo -e "${red}ERROR:${NC} Attempt to share $share_export via NFS would create a conflict in ${yellow}/etc/exports${NC}." echo -e " To solve the problem you can override the default Docksal projects folder." echo -e " by setting ${yellow}DOCKSAL_NFS_PATH${NC} value in ${yellow}$CONFIG_ENV${NC}." sleep 1 echo echo-green "Attempting auto-fix:" _confirm "Do you want to use ${yellow}$HOME/Projects${NC} as Docksal projects path?" # Write proposed DOCKSAL_NFS_PATH and try remounting nfs mkdir -p "$HOME/Projects" echo -e "\nDOCKSAL_NFS_PATH=\"$HOME/Projects\"\n" | tee -a ${CONFIG_ENV} >/dev/null # Remove extra new lines cat -s ${CONFIG_ENV} > ${CONFIG_ENV} docker_machine_mount_nfs "$HOME/Projects:$HOME/Projects" return $? fi # TODO: move exports into a separate function and make it work with D4M with or without VirtualBox. exports="${exports}$share_export 127.0.0.1 $machine_ip -alldirs -maproot=0:0\n" done exports="${exports}${exports_close}" local new_exports=$(echo -e "$exports") local old_exports=$(cat /etc/exports 2>/dev/null) if [[ "$new_exports" != "$old_exports" ]]; then # Write temporary exports file to /tmp/etc.exports.XXXXX and check it local exports_test="/tmp/etc.exports.$RANDOM" echo -e "$exports" | tee "$exports_test" >/dev/null exports_errors=$(nfsd -F "$exports_test" checkexports 2>&1) rm -f "$exports_test" >/dev/null 2>&1 # Do not write /etc/exports if there are config check errors if [[ "$exports_errors" != '' ]]; then echo-error "$exports_errors" echo "-----------------" echo -e "$exports" echo "-----------------" return 1 fi echo "Writing /etc/exports..." echo -e "$exports" | sudo tee /etc/exports >/dev/null NFSD_RESTART_REQUIRED="true" else # /etc/export already contains what is required echo "NFS shares are already configured" fi # Start/Restart nfsd (ps aux | grep /sbin/nfsd | grep -v grep >/dev/null) && NFSD_IS_RUNNING="true" if [[ "$NFSD_IS_RUNNING" == "true" ]]; then if [[ "$NFSD_RESTART_REQUIRED" == "true" ]]; then echo-green "Restarting nfsd..." sudo nfsd restart sleep 5 fi else echo-green "Starting nfsd..." sudo nfsd start sleep 10 fi # Mount exported folders echo-green "Mounting NFS shares..." # Start NFS client on docker-machine docker-machine ssh "$machine_name" \ "sudo /usr/local/etc/init.d/nfs-client start" for share in ${ARGV[@]}; do local share_=(${share//:/ }); local share_export="${share_[0]}" local share_mount="${share_[1]}" # Add new exports echo "Mounting local $share_export to $share_mount" # Timeout will break mount command, that can freeze if connection to host is blocked by firewall docker-machine ssh "$machine_name" \ "sudo mkdir -p $share_mount ; timeout -t 5 sudo umount $share_mount 2>/dev/null ; timeout -t 20 sudo mount -t nfs -o nolock,noacl,nocto,noatime,nodiratime,actimeo=1 $nfs_ip:$share_export $share_mount" # [!!] Think twice about performance before changing nfs settings if [ ! $? -eq 0 ]; then echo "Retrying mounting local $share_export to $share_mount" sleep 5 docker-machine ssh "$machine_name" \ "timeout -t 20 sudo mount -t nfs -o nolock,noacl,nocto,noatime,nodiratime,actimeo=1 $nfs_ip:$share_export $share_mount" if [[ $? != 0 ]]; then echo-error "NFS share mount failed." "Try ${yellow}fin vm restart${NC}." return 1 fi fi done } docker_machine_remove_nfs () { local machine_name="$DOCKER_MACHINE_NAME" # Remove our own old exports local exports_open="# /dev/null | \ tr "\n" "\r" | \ sed "s/${exports_open}.*${exports_close}//" | \ tr "\r" "\n" ) # Write temporary exports file to /tmp/etc.exports.XXXXX and check it local exports_test="/tmp/etc.exports.$RANDOM" echo -e "$exports" | tee "$exports_test" >/dev/null exports_errors=$(nfsd -F "$exports_test" checkexports 2>&1) rm -f "$exports_test" >/dev/null 2>&1 # Do not write /etc/exports if there are config check errors if [[ "$exports_errors" != '' ]]; then echo-error "$exports_errors" echo "-----------------" echo -e "$exports" echo "-----------------" return 1 fi echo "Writing and applying /etc/exports..." echo -e "$exports" | sudo tee /etc/exports >/dev/null sudo nfsd restart sleep 2 } # Fix/optimize Windows network settings for file sharing. # See http://serverfault.com/a/236393 for details. smb_windows_fix() { ! is_windows && return echo-green "Going to optimize Windows network settings for file sharing..." echo "You may see an elevated command prompt - click Yes." sleep 2 local tmpfile="/tmp/fix-smb.reg" cat < $tmpfile Windows Registry Editor Version 5.00 [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management] "LargeSystemCache"=dword:00000001 [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\LanmanServer\Parameters] "Size"=dword:00000003 EOF # Update registry and restart Server and Computer Browser services. winsudo "regedit /S $(cygpath -w ${tmpfile}) && net stop server /y && net start server && net start browser" } # Method to create the user and SMB network share on Windows. # @param share_name, share_path smb_share_add() { local share_name=$1 local share_path=$2 # Add SMB share if it does not exist. if [[ "$(net share)" != *"${share_name}"*"Docksal ${share_path} drive"* ]]; then command_share="net share ${share_name}=${share_path} /grant:${USERNAME},FULL /REMARK:\"Docksal ${share_path} drive\"" echo-green "Adding docker SMB share..." winsudo "$command_share" if [[ $? != 0 ]]; then echo-error "SMB share creation failed." \ "Share name: ${share_name}" \ "Share path: ${share_path}" return 1 fi fi } # Method to mount the SMB share in Docker machine. # @param share_name, mount_point, password smb_share_mount() { local share_name=$1 local mount_point=$2 local password=$3 # single quotes are banned from being used in password password=${password//\'/} network_id=$("$vboxmanage" showvminfo ${DOCKER_MACHINE_NAME} --machinereadable | grep hostonlyadapter | cut -d'"' -f2) DOCKER_HOST_IP=$("$vboxmanage" list hostonlyifs | grep "${network_id}$" -A 3 | grep IPAddress |cut -d ':' -f2 | xargs) # User ntlmssp by default. For some Windows machines ntlm should be used instead # Also see https://unix.stackexchange.com/questions/124342/mount-error-13-permission-denied/124352#124352 SMB_SEC="${SMB_SEC:-ntlmssp}" # Doing unmount first to make it possible to rerun this command without restarting the VM (e.g., when debugging) # Specifying domain when mounting. May help when a Windows machine is in a domain environment. command_mount=" (sudo umount $mount_point >/dev/null 2>&1 || true) && \ sudo mkdir -p $mount_point && \ sudo mount -t cifs -o username='${USERNAME}',pass='${password}',domain='${USERDOMAIN}',sec='${SMB_SEC}',nobrl,mfsymlinks,noperm,actimeo=1,dir_mode=0777,file_mode=0777 //${DOCKER_HOST_IP}/$share_name $mount_point" docker-machine ssh ${DOCKER_MACHINE_NAME} "$command_mount" # Perform a second attenpt with sec=ntlm if the first one (with ntslssp or custom override via SMB_SEC=) failed. if [[ $? != 0 ]]; then echo-yellow "Mount command failed... Trying an alternative method..." # Switch sec to ntlm and try again command_mount=$(echo $command_mount | sed 's/sec=.*,/sec=ntlm,/') docker-machine ssh ${DOCKER_MACHINE_NAME} "$command_mount" if [[ $? == 0 ]]; then echo-green 'Success!' else echo-error "SMB share mount failed." \ "Make sure your Windows password is correct and run ${yellow}fin vm restart${NC} to try again." \ "For troubleshooting instructions see ${yellow}https://docs.docksal.io/en/master/troubleshooting-smb${NC}" return 1 fi fi } docker_machine_mount_smb () { # add share \\hostname\c writable to current user # ask user for his password # use his password for mount -t cifs ... # Get a list of logical drives (type 3 = Local disk). local drives=$(wmic logicaldisk get drivetype,name | grep 3 | awk '{print $2}' | sed 's/\r//g') read -s -p "Enter your Windows account password: " password echo # Add a new line after user input. if ([[ "$password" =~ \' ]] || [[ "$password" =~ , ]] || [[ "$password" =~ \\ ]]); then echo-error "Password contains illegal characters" \ "Your password contains a single quote ('), a comma (,) or a back slash (\\)" \ "Please change you password and run ${yellow}fin vm restart${NC}" return 1 fi for drive in ${drives}; do local mount_point=$(cygpath -u "$drive" | sed 's/^\/cygdrive\///') # Avoid conflicts with existing drive shares buy using a unique share name local share_name="docksal-$mount_point" smb_share_add "$share_name" "$drive" && smb_share_mount "$share_name" "/$mount_point" "$password" done } docker_machine_remove_smb () { local drives=$(wmic logicaldisk get drivetype,name | grep 3 | awk '{print $2}' | sed 's/\r//g') for drive in ${drives}; do local mount_point=$(cygpath -u "$drive" | sed 's/^\/cygdrive\///') # Avoid conflicts with existing drive shares buy using a unique share name local share_name="docksal-$mount_point" command_share="net share ${share_name} /DELETE" winsudo "$command_share" done } docker_machine_mounts () { if is_mac; then echo-green "Configuring NFS shares..." local MOUNT_POINT="${DOCKSAL_NFS_PATH:-/Users}" docker_machine_mount_nfs "$MOUNT_POINT:$MOUNT_POINT" elif is_windows; then echo-green "Configuring SMB shares..." docker_machine_mount_smb fi } # Creates bind mounts on WSL from /mnt/c to /c so that docker-compose pth would match path inside docker # See overridden pwd function too wsl_mount_drives () { ! is_wsl && return; local mounts=$(mount) # Create mnt point binds if they don't exist for drive in $(ls "/mnt"); do ! (echo "$mounts" | grep "on /$drive" >/dev/null) && \ echo "Creating bind mount /mnt/$drive -> /$drive" && \ sudo mkdir -p "/$drive" && \ sudo mount --bind "/mnt/$drive" "/$drive" done } #------------------------------- VM Commands ----------------------------------- vm () { if is_docker_native; then echo "Docksal VM cannot be used with Docker for Mac/Docker for Windows" return 1 fi if is_linux; then echo "Docksal VM should not be used on Linux" return 1 fi check_binary_found 'docker-machine' if ! is_docker_machine_exist && [[ "$1" != "" ]] && [[ "$1" != "start" ]] && [[ "$1" != "create" ]] && [[ "$1" != "ls" ]] && [[ "$1" != "remove" ]] && [[ "$1" != "rm" ]] && [[ "$1" != "active" ]] ; then echo-yellow "Docker machine $DOCKER_MACHINE_NAME is not created. Use 'fin vm start' or 'fin up'." return 1 fi case $1 in create) # usage: fin vm create shift # errors handling [[ "$1" == "" ]] && echo "Please provide a name for a new vm." && return 1 is_docker_machine_exist "$1" && echo "Machine ${1} already exists." && return 1 # create docker_machine_create "$1" ;; active) # Get/Set active vm shift if [[ "$1" == "" ]]; then echo "$DOCKER_MACHINE_NAME" else [[ "$1" == "$DOCKER_MACHINE_NAME" ]] && echo "$1 is already active" && return 0 ! is_docker_machine_exist "$1" && echo "No docker machine with name $1" && return 1 mkdir -p "$CONFIG_MACHINES" || return 1 echo "$1" | tee "$CONFIG_MACHINE_ACTIVE" >/dev/null && echo "$1 is set active" fi ;; start) if is_docker_machine_running; then echo "Machine \"$DOCKER_MACHINE_NAME\" is already running." else docker_machine_start fi ;; restart) docker_machine_stop; docker_machine_start ;; status) shift local machine_name="${1:-$DOCKER_MACHINE_NAME}" docker-machine status "$machine_name" ;; stop) # Disable resolver configuration before machine is turned off configure_resolver off docker_machine_stop ;; ssh) shift docker-machine ssh "$DOCKER_MACHINE_NAME" "$@" ;; stats) vm-stats ;; kill) # Disable resolver configuration before machine is turned off configure_resolver off shift local machine_name="${1:-$DOCKER_MACHINE_NAME}" docker-machine kill "$machine_name" ;; ls|list) docker-machine ls ;; remove|rm) # Disable resolver configuration before machine is turned off configure_resolver off shift local machine_name="${1:-$DOCKER_MACHINE_NAME}" docker_machine_remove "$machine_name" ;; mount) docker_machine_mounts ;; ip) shift local machine_name="${1:-$DOCKER_MACHINE_NAME}" if [[ "$1" =~ [0-9][0-9]*[0-9]*.[0-9][0-9]*[0-9]*\.[0-9][0-9]*[0-9]*\.[0-9] ]]; then # set ip echo-green "Setting ip to $1..." # echo "$1" > "$DOCKER_MACHINE_FOLDER/ip" # echo-yellow "IP change will take effect on next machine start." DOCKER_MACHINE_IP="$1" docker_machine_change_ip else docker-machine ip "$machine_name" fi ;; env) shift DOCKER_MACHINE_NAME="${1:-$DOCKER_MACHINE_NAME}" docker-machine env "$DOCKER_MACHINE_NAME" ;; ram) shift vm-ram "$@" ;; hdd) shift vm-hdd "$@" ;; help) show_help_vm ;; regenerate-certs) docker-machine regenerate-certs -f "$DOCKER_MACHINE_NAME" vm restart ;; *) show_help_vm return 1 ;; esac } # TODO: deprecated. Remove as this command is not very useful right now. vm-stats () { check_docker_running # should be running check_vbox_version metrics="CPU/Load/User,CPU/Load/Kernel,Net/Rate/Rx,Net/Rate/Tx" "$vboxmanage" metrics setup --period 1 --samples 1 "$DOCKER_MACHINE_NAME" sleep 1 "$vboxmanage" metrics query "$DOCKER_MACHINE_NAME" "$metrics" } vm-ram () { echo "Total $("$vboxmanage" showvminfo "$DOCKER_MACHINE_NAME" | grep Memory)" if [[ "$1" != "" ]]; then local running is_docker_machine_running && running=1 _confirm "Continue changing Memory size to ${1}MB?" [[ ${running} == 1 ]] && docker_machine_stop "$vboxmanage" modifyvm "$DOCKER_MACHINE_NAME" --memory "$1" && \ echo "Memory size updated." [[ ${running} == 1 ]] && docker_machine_start else output=$(fin vm ssh free -m) echo -n "Available Memory: " echo "$output" | grep "buffers/cache" | awk '{ print $4, "MB" }' echo -n "Available Swap: " echo "$output" | grep "Swap" | awk '{ print $4, "MB" }' fi } vm-hdd () { docker-machine ssh "$DOCKER_MACHINE_NAME" df -h /mnt/sda1 } #------------------------------- Other Commands ----------------------------------- # Start containers up () { check_docksal_environment # Run ssh_add here, since there is no better place to get it triggered on Linux and Docker for Mac/Win # This should not be the last command in a function as it will exit 1 on non Linux/"native" setups. ( is_linux || is_docker_native ) && ssh_add # On macOS check that project is reachable to Docker check_macos_path # Check that project name is unique since non-unique names will give problems with docker-compose check_project_unique # Temporary workaround for NFS issues on Mac # See https://github.com/docksal/docksal/issues/265 is_mac && find . -type d > /dev/null 2>&1 # Fix project root permissions if necessary set_project_root_permissions # Start containers _start_containers || return 1 # If Unisons is used, wait for it to sync unison_sync_wait echo-green "Virtual Host: ${yellow}http://$VIRTUAL_HOST${NC}" } # Stop containers stop () { # Do not do any checks for "stop all projects" action if [[ $1 == '-a' ]] || [[ $1 == '--all' ]]; then _stop_containers "$@" return $? fi _stop_containers "$@" } # Restart container(s) # @param $1 container_name restart () { check_docksal_environment # Run ssh_add here, since there is no better place to get it triggered on Linux and Docker for Mac/Win # This should not be the last command in a function as it will exit 1 on non Linux/"native" setups. ( is_linux || is_docker_native ) && ssh_add # Check that project name is unique since non-unique names will give problems with docker-compose check_project_unique # Fix project root permissions if necessary set_project_root_permissions _restart_containers "$@" unison_sync_wait } # Remove container(s) # @param $1 $2... container names remove () { check_project_environment check_project_unique eval $(parse_params "$@") # Alert when no containers specified and no force passed if [[ "${ARGV[0]}" == "" ]] && [[ "$f" != "f" ]] && [[ "$force" != "force" ]]; then echo-alert "Removing containers and volumes of ${yellow}$COMPOSE_PROJECT_NAME${NC}" _confirm "Continue?"; fi # Removes containers _remove_containers $(echo "${ARGV[*]}") } # Reset container(s) (stop, remove, up) # @param $1 $2... containers names reset () { check_docksal_environment # Support quiet removal if [[ $1 == "-f" ]]; then shift remove -f "$@" else remove "$@" fi # Return here if there are errors with remove # This also helps with cases when there was a typo in the reset command, like "fin reset ssystem" local ret=$? [[ "$ret" != 0 ]] && return ${ret} # Fix project root permissions if necessary set_project_root_permissions # Run ssh_add here, since there is no better place to get it triggered on Linux and Docker for Mac/Win ( is_linux || is_docker_native ) && [[ "$@" == "" ]] && ssh_add _start_containers || return 1 unison_sync_wait } # List project containers project_status () { check_docksal_environment check_project_unique docker-compose ps } # List Docksal projects # @param $1 Show containers from all projects (-a) project_list () { local all if [[ "$1" == "-a" ]] || [[ "$1" == "--all" ]]; then all="--all" fi check_docker_running docker ps ${all} \ --filter 'label=io.docksal.project-root' \ --format 'table {{.Label "com.docker.compose.project"}}\t{{.Status}}\t{{.Label "io.docksal.virtual-host"}}\t{{.Label "io.docksal.project-root"}}' } project_create () { eval $(parse_params "$@") # Read name from params if available local project_name="$name" # Determine project name if [[ "$project_name" == "" ]]; then while [[ "$project_name" == "" ]]; do echo -en "${yellow}1. Name your project${NC} (lowercase alphanumeric, underscore, and hyphen): " read -p "" project_name # This will not accept project name of 1 symbol long. Not sure if this needs to be fixed if [[ ! "$project_name" =~ ^[a-z0-9][a-z0-9_-]*[a-z0-9]$ ]]; then echo-error "Unacceptable project name" \ "Use only lowercase letters, numbers, underscores, and hyphens." \ "Project name defines host name, where underscores will be replaced with hyphens." \ "" project_name="" fi done echo '' fi local project_virtual_host_name="$(echo ${project_name} | sed 's/_/-/g' | sed 's/[^-a-z0-9]//g').${DOCKSAL_DNS_DOMAIN}" # Determine target installation software if [[ "$choice" == "" ]]; then echo -e "${yellow}2. What would you like to install?${NC}" echo -e "${blue} PHP based${NC}" echo -e "${green} 1.${NC} Drupal 8" echo -e "${green} 2.${NC} Drupal 8 (Composer Version)" echo -e "${green} 3.${NC} Drupal 7" echo -e "${green} 4.${NC} Wordpress" echo -e "${green} 5.${NC} Magento" echo -e "${green} 6.${NC} Laravel" echo -e "${green} 7.${NC} Symfony Skeleton" echo -e "${green} 8.${NC} Symfony WebApp" echo -e "${green} 9.${NC} Grav CMS" echo -e "${green} 10.${NC} Backdrop CMS" echo echo -e "${acqua} Go based${NC}" echo -e "${green} 11.${NC} Hugo" echo echo -e "${lime} JS based${NC}" echo -e "${green} 12.${NC} Gatsby JS" echo echo -e "${magenta} HTML${NC}" echo -e "${green} 13.${NC} Static HTML site" echo -e "" while [[ "$choice" == "" ]]; do read -p "Enter your choice (1-13): " choice if ! (( "$choice" >= "1" && "$choice" <= 13 )); then choice="" echo 'Enter a number between 1 and 13.' fi done fi # Set target repo case ${choice} in 1) target_cms="Drupal 8" target_host_name_search_string="drupal8.docksal" target_repo="$URL_REPO_DRUPAL8" ;; 2) target_cms="Drupal 8 (Composer Version)" target_host_name_search_string="drupal8.docksal" target_repo="$URL_REPO_DRUPAL8COMPOSER" ;; 3) target_cms="Drupal 7" target_host_name_search_string="drupal7.docksal" target_repo="$URL_REPO_DRUPAL7" ;; 4) target_cms="Wordpress" target_host_name_search_string="wordpress.docksal" target_repo="$URL_REPO_WORDPRESS" ;; 5) target_cms="Magento" target_host_name_search_string="magento.docksal" target_repo="$URL_REPO_MAGENTO" ;; 6) target_cms="Laravel" target_host_name_search_string="" target_repo="$URL_REPO_LARAVEL" ;; 7) target_cms="Symfony Skeleton" target_host_name_search_string="symfony.docksal" target_repo="$URL_REPO_SYMFONY_SKELETON" ;; 8) target_cms="Symfony WebApp" target_host_name_search_string="symfony.docksal" target_repo="$URL_REPO_SYMFONY_WEBAPP" ;; 9) target_cms="Grav CMS" target_host_name_search_string="" target_repo="$URL_REPO_GRAV" ;; 10) target_cms="Backdrop CMS" target_host_name_search_string="backdrop.docksal" target_repo="${URL_REPO_BACKDROP}" ;; 11) target_cms="Hugo" target_host_name_search_string="" target_repo="$URL_REPO_HUGO" ;; 12) target_cms="Gatsby JS" target_host_name_search_string="" target_repo="$URL_REPO_GATSBY" ;; 13) target_cms="Plain HTML" target_host_name_search_string="" target_repo="" ;; esac echo echo -e "Project folder: ${yellow}$(pwd)/$project_name${NC}" echo -e "Project software: ${yellow}${target_cms}${NC}" echo -e "Project URL: ${yellow}http://${project_virtual_host_name}${NC}" echo if [[ "$y" != "y" && "$yes" != "yes" ]]; then _confirm "Do you wish to proceed?" fi if [[ "$target_cms" == "Plain HTML" ]]; then mkdir "${project_name}" && cd "${project_name}" || exit 1 mkdir ".docksal" mkdir "docroot" echo "Hello, Docksal!" > "docroot/index.html" echo 'DOCKSAL_STACK="default-nodb"' >> ".docksal/docksal.env" fin up echo -e "Done! Visit ${yellow}http://${project_virtual_host_name}${NC}" exit fi echo -e "${green}Cloning repository...${NC}" run_cli git clone -b master "${target_repo}" "${project_name}" [ ! $? -eq 0 ] && _confirm "Checkout finished with errors. Do you wish to continue?" echo -e "${yellow}3. Installing site${NC}" [[ -f "${project_name}/.docksal/docksal.env" ]] && ( cd "${project_name}/.docksal" # Edit docksal.env to use a custom user-supplied host name sed -i.bak "s/VIRTUAL_HOST=${target_host_name_search_string}/VIRTUAL_HOST=${project_virtual_host_name}/g" docksal.env rm -f docksal.env.bak > /dev/null 2>&1 ) cd "${project_name}" fin init } # Remove containers and erase files project_erase () { check_project_environment check_project_unique eval $(parse_params "$@") # Alert when force passed if [[ "$f" != "f" ]] && [[ "$force" != "force" ]]; then echo-alert "DESTRUCTIVE OPERATION" \ "Removing containers of $COMPOSE_PROJECT_NAME and ${magenta}removing all files in $PROJECT_ROOT${NC}" _confirm " Continue?"; fi # Remove containers remove --force # Remove files after some safety checks if [[ "$PROJECT_ROOT" != "" ]] && [[ "$PROJECT_ROOT" != "." ]] && [[ "$PROJECT_ROOT" != "/c" ]] && [[ "$PROJECT_ROOT" != "/" ]] && [[ "$PROJECT_ROOT" != "$HOME" ]]; then echo "Removing $PROJECT_ROOT..." rm -rf "$PROJECT_ROOT/" if [[ "$?" != "0" ]] && [[ "$f" != "f" ]] && [[ "$force" != "force" ]]; then echo "..." _confirm "Some files were not removed. Try with 'sudo'?" fi sudo rm -rf "$PROJECT_ROOT/" fi } # Resets Docksal system services system_reset () { # TODO: add /etc/exports, etc. here for a complete reset of Docksal # Configure network has to happen before any communications with the docker daemon happen (before check_docker_running) if [[ "$1" == "" ]] ; then configure_network echo-green 'Resetting Docksal services...' echo ' ➔ proxy' install_proxy_service echo ' ➔ dns' install_dns_service configure_resolver echo ' ➔ ssh-agent' install_sshagent_service return fi # Configure network has to happen before any communications with the docker daemon happen (before check_docker_running) if [[ "$1" == "network" ]] ; then configure_network return fi if [[ "$1" == "vhost-proxy" ]] ; then echo-green 'Resetting Docksal HTTP/HTTPS reverse proxy service...' install_proxy_service return fi if [[ "$1" == "dns" ]] ; then echo-green 'Resetting Docksal DNS service and configuring resolver for .docksal domain...' install_dns_service configure_resolver return fi if [[ "$1" == "ssh-agent" ]] ; then echo-green 'Resetting Docksal ssh-agent service...' install_sshagent_service return fi } # Displays Docksal system status system_status () { # TODO: replace with a custom view showing services, networks, vm, etc. status docker ps --filter "label=io.docksal.group=system" } # Starts all Docksal related things # TODO: add projects, network, /etc/exports, etc. here for a complete shutdown of Docksal system_start () { if ! is_linux && ! is_docker_native && ! is_wsl; then echo "Starting Docksal VM..." vm start else echo "Starting Docksal system services..." system_reset fi } # Stops/disables all Docksal things # TODO: add projects, /etc/exports, etc. here for a complete shutdown of Docksal system_stop () { if ! is_linux && ! is_docker_native && ! is_wsl; then echo-green "Stopping Docksal VM..." # VM stop will perform configure_resolver off vm stop else # keep in a subshell so that exit during that command would not stop other command in system stop (_stop_containers --all) echo-green "Stopping Docksal system services..." docker ps -q --filter "label=io.docksal.group=system" | xargs docker rm -vf >/dev/null configure_resolver off configure_network off fi } # Add key to ssh-agent or run ssh-add with provided @param # @param $1 -D, -l or path to custom key ssh_add () { check_winpty_found # Check if ssh-agent container is running # Note: no spaces in format. PWD trips over this when docker is run with sudo (during Docksal install/update). local running=$(docker inspect --format="{{.State.Running}}" docksal-ssh-agent 2>/dev/null) # Start the ssh-agent if not running if [[ "$running" != "true" ]]; then echo-error "docksal-ssh-agent is not running" \ "Please report this issue, as this should not usually happen." \ "Run ${yellow}fin system reset ssh-agent${NC} to start the service." # We could automatically run install_sshagent_service here, however that may lead to an endless recursion # if something is terribly wrong. So it's better to alret the user and ask them to try a manual fix. return 1 fi local ssh_path="$HOME/.ssh" # On Windows docker and docker-compose expect a Windows style path is_windows && ssh_path=$(cygpath -w "$ssh_path") # Ensure ssh_path exists mkdir -p "$ssh_path" if ! is_tty; then # In a non-interactive environment (e.g., CI) run non-interactively. Keys with passphrases cannot be used! echo-yellow "Running in a non-interactive environment. SSH keys with passphrases cannot be used." fi # Only do this when given an argument (key name) or nothing, but not for options (e.g., -l) if [[ ! "${1}" =~ ^- ]]; then # Copy all keys from the host to the ssh-agent container docker cp "$ssh_path" docksal-ssh-agent:/ # Add SSH Keys defined in environment variables eval 'ssh_keys=(${!SECRET_SSH_KEY_@})' for ssh_key in "${ssh_keys[@]}" do local ssh_key_name=${!ssh_key} local new_ssh_key="${ssh_path}/${ssh_key_name}" # Load the requested key. If no key provided, ssh-add will load all default keys (id_rsa, id_dsa, id_ecdsa) if is_tty; then # Run ssh-add interactively to allow entering a passphrase for ssh keys (if set) ${winpty} docker exec -it docksal-ssh-agent /run.sh ssh-add "${ssh_key_name}" else docker exec docksal-ssh-agent /run.sh ssh-add "${ssh_key_name}" fi done fi # Load the requested key. If no key provided, ssh-add will load all default keys (id_rsa, id_dsa, id_ecdsa) if is_tty; then # Run ssh-add interactively to allow entering a passphrase for ssh keys (if set) ${winpty} docker exec -it docksal-ssh-agent /run.sh ssh-add "$@" else docker exec docksal-ssh-agent /run.sh ssh-add "$@" fi local ret=$? # Remove copied keys from the ssh-agent container after loading into the ssh-agent # Only do this when given an argument (key name) or nothing, but not for options (e.g., -l) [[ "${1#-}" == "$1" ]] && docker exec docksal-ssh-agent rm -rf /.ssh /root/.ssh return "$ret" } #----- Installations and updates ----- # Reports version stats stats_ping () { [[ "$DOCKSAL_STATS_OPTOUT" != 0 ]] && return # Don't run if the user opted out # We are passing OS version via a custom User-Agent request header, which GA can parse local user_agent is_linux && user_agent="fin/${FIN_VERSION} (Linux ${OS_NAME}-${OS_VERSION})" is_mac && user_agent="fin/${FIN_VERSION} (Macintosh Intel Mac OS X ${OS_VERSION})" is_windows && user_agent="fin/${FIN_VERSION} (Windows NT ${OS_VERSION})" # Local instances local source='Local' # CI instances is_ci && source='CI' # Play with Docker instances is_pwd && source='PWD' # Katacoda instances is_katacoda && source='Katacoda' curl -kfsL \ --user-agent "$user_agent" \ --data "v=1&tid=${DOCKSAL_STATS_TID}&cid=${DOCKSAL_UUID}&t=screenview&an=fin&av=${FIN_VERSION}&cs=${source}&cd=ping" \ "${DOCKSAL_STATS_URL}" >/dev/null 2>&1 & return 0 } # Configure Docksal network settings on Alpine via ip addr configure_network_alpine () { local mode="${1:-on}" # Determine current status local status (grep -q "lo:docksal" <<< "$(ip addr show dev lo)") && status="enabled" || status="disabled" if [[ "$mode" == "on" && "$status" == "disabled" ]] ; then # Add an IP address alias to the loopback interface # This will not persist a reboot or interface change sudo ip addr add ${DOCKSAL_SUBNET} dev lo label lo:docksal elif [[ "$mode" == "off" && "$status" == "enabled" ]]; then # Remove the IP address alias sudo ip addr del ${DOCKSAL_SUBNET} dev lo fi } # Configure Docksal network settings on Mac configure_network_mac () { local mode="${1:-on}" # Determine current status local status (grep -q "$DOCKSAL_IP" <<< "$(ifconfig lo0)") && status="enabled" || status="disabled" if [[ "$mode" == "on" && "$status" == "disabled" ]] ; then # Add Docksal IP aliases on the local loopback adapter sudo ifconfig lo0 alias ${DOCKSAL_IP} 255.255.255.0 sudo ifconfig lo0 alias ${DOCKSAL_HOST_IP} 255.255.255.0 elif [[ "$mode" == "off" && "$status" == "enabled" ]]; then # Remove Docksal IP alias from the local loopback adapter sudo ifconfig lo0 -alias ${DOCKSAL_IP} >/dev/null 2>&1 sudo ifconfig lo0 -alias ${DOCKSAL_HOST_IP} >/dev/null 2>&1 fi } # Configure Docksal network settings on Windows configure_network_windows () { local mode="${1:-on}" # Docker for Win if [[ "$mode" == "on" ]]; then # Add Docksal IP aliases on the DockerNAT adapter local command1="netsh interface ip add address name=\"vEthernet (DockerNAT)\" addr=${DOCKSAL_IP} mask=255.255.255.0" local command2="netsh interface ip add address name=\"vEthernet (DockerNAT)\" addr=${DOCKSAL_HOST_IP} mask=255.255.255.0" winsudo "$command1 && $command2" elif [[ "$mode" == "off" ]]; then # Remove Docksal IP aliases on the DockerNAT adapter local command1="netsh interface ip delete address name=\"vEthernet (DockerNAT)\" addr=${DOCKSAL_IP}" local command1="netsh interface ip delete address name=\"vEthernet (DockerNAT)\" addr=${DOCKSAL_HOST_IP}" winsudo "$command1 && $command2" fi # Wait 5s for network configuration to apply sleep 5 } # Configure Docksal network settings on WSL configure_network_wsl () { # TODO: implement on/off modes local mode="${1:-on}" cmd.exe /c netsh interface ip add address name=\"vEthernet \(DockerNAT\)\" address="${DOCKSAL_IP}" mask="255.255.255.0" cmd.exe /c netsh interface ip add address name=\"vEthernet \(DockerNAT\)\" address="${DOCKSAL_HOST_IP}" mask="255.255.255.0" # Wait 5s for network configuration to apply sleep 5 } # Configure Docksal network settings configure_network () { local mode="${1:-on}" # Set mode to off for Mac and Windows when not running in "native" mode if ! is_docker_native && (is_mac || is_windows); then mode="off" fi if [[ "$mode" == "on" ]]; then echo-green "Configuring network settings..." else echo-green "Restoring network settings..." fi is_linux && configure_network_alpine "$mode" && return $? is_mac && configure_network_mac "$mode" && return $? is_windows && configure_network_windows "$mode" && return $? is_wsl && configure_network_wsl "$mode" && return $? } # Start system-wide vhost-proxy service install_proxy_service () { docker rm -f docksal-vhost-proxy >/dev/null 2>&1 || true docker volume rm docksal_projects >/dev/null 2>&1 || true # Create a bind mount volume to the projects directory if defined and exists. Otherwise, create an empty volume. if [[ -d "$PROJECTS_ROOT" ]]; then # No quotes around ${PROJECTS_ROOT} below or the volume will not work correctly!!! docker volume create --name docksal_projects --opt type=none --opt device=${PROJECTS_ROOT} --opt o=bind >/dev/null 2>&1 else docker volume create --name docksal_projects >/dev/null 2>&1 # Warn if PROJECTS_ROOT was set but is not a valid directory [[ "$PROJECTS_ROOT" != "" ]] && echo-warning "PROJECTS_ROOT ($PROJECTS_ROOT) is not a valid directory" fi # Open up vhost-proxy to the world in CI/Sandbox/PWD environments ( is_ci || is_pwd || is_katacoda ) && DOCKSAL_VHOST_PROXY_IP="0.0.0.0" # Mount custom certs folder if available local mount_certs [[ -d ${CONFIG_CERTS} ]] && mount_certs="--mount type=bind,src=${CONFIG_CERTS},dst=/etc/certs/custom" # PROJECT_INACTIVITY_TIMEOUT, PROJECT_DANGLING_TIMEOUT and PROJECTS_ROOT (docksal_projects volume) are used in CI # and are inactive unless configured. # PROJECT_INACTIVITY_TIMEOUT - defines the timeout of inactivity after which the project stack will be stopped (e.g., 0.5h) # PROJECT_DANGLING_TIMEOUT - defines the timeout of inactivity after which the project stack and code base will be # entirely wiped out from the host (e.g., 168h). WARNING: use at your own risk! docker run -d --name docksal-vhost-proxy --label "io.docksal.group=system" --restart=always \ -p "${DOCKSAL_VHOST_PROXY_IP:-$DOCKSAL_IP}:${DOCKSAL_VHOST_PROXY_PORT_HTTP:-80}":80 -p "${DOCKSAL_VHOST_PROXY_IP:-$DOCKSAL_IP}:${DOCKSAL_VHOST_PROXY_PORT_HTTPS:-443}":443 \ -e ACCESS_LOG="${DOCKSAL_VHOST_PROXY_ACCESS_LOG:-0}" \ -e DEBUG_LOG="${DOCKSAL_VHOST_PROXY_DEBUG_LOG:-0}" \ -e STATS_LOG="${DOCKSAL_VHOST_PROXY_STATS_LOG:-0}" \ -e PROJECT_INACTIVITY_TIMEOUT="${PROJECT_INACTIVITY_TIMEOUT:-0}" \ -e PROJECT_DANGLING_TIMEOUT="${PROJECT_DANGLING_TIMEOUT:-0}" \ --mount type=volume,src=docksal_projects,dst=/projects \ --mount type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \ ${mount_certs} \ "${IMAGE_VHOST_PROXY}" >/dev/null } # Detects upstream DNS or falls back to DOCKSAL_DEFAULT_DNS # If DOCKSAL_DNS_UPSTREAM is preset, then skips DNS detection detect_dns () { # Do not detect, if overridden in docksal.env [[ "$DOCKSAL_DNS_UPSTREAM" != "" ]] && return if is_linux || is_mac; then # Get from /etc/resolv.conf nameservers=$(grep "nameserver" "/etc/resolv.conf" 2>/dev/null | sed "s/nameserver //") # Find first suitable if [[ "$nameservers" != "" ]]; then for server in $nameservers; do # Check that it is an IP address and exclude Docksal IP if [[ "$server" =~ [0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3} ]] && [[ "$server" != "$DOCKSAL_IP" ]]; then DOCKSAL_DNS_UPSTREAM="$server" fi done else # In case no name servers found, fall back to the default DOCKSAL_DNS_UPSTREAM="$DOCKSAL_DEFAULT_DNS" fi elif is_windows; then # Look for Primary WINS server, it represents DNS whe WINS services is in use local primary_wins=$(ipconfig /all | grep "Primary WINS") if [[ "$primary_wins" =~ ([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}) ]]; then DOCKSAL_DNS_UPSTREAM=${BASH_REMATCH[0]} else # In case nothing was found, fall back to the default DOCKSAL_DNS_UPSTREAM="$DOCKSAL_DEFAULT_DNS" fi fi # export to variable and return (for direct usage or debug) export DOCKSAL_DNS_UPSTREAM } # Start system-wide dns service install_dns_service () { detect_dns # Stop and remove existing container docker rm -f docksal-dns >/dev/null 2>&1 || true # This container cannot use the host's resolver configuration. The host itself may be configured to use this # container for DNS resolution (Windows and Linux), so we will get into a recursion. # DOCKSAL_DNS_UPSTREAM can be set to the controlled network DNS server address (VPN/LAN) in the global docksal.env file. # When user disconnects from that network, the backup DNS will handle name resolution (assuming it is reachable). echo " upstream $DOCKSAL_DNS_UPSTREAM" docker run -d --name docksal-dns --label "io.docksal.group=system" --restart=always \ -p "${DOCKSAL_IP}:53:53/udp" --cap-add=NET_ADMIN --dns="$DOCKSAL_DNS_UPSTREAM" --dns="9.9.9.9" \ -e DNS_IP="$DOCKSAL_IP" -e DNS_DOMAIN="$DOCKSAL_DNS_DOMAIN" -e LOG_QUERIES="$DOCKSAL_DNS_DEBUG" \ --mount type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \ "${IMAGE_DNS}" >/dev/null } # Configure system-wide *.docksal resolver using /etc/resolver configure_resolver_mac () { local mode="${1:-on}" if [[ "$mode" == "on" ]]; then # Check whether resolver is already configured if ! (grep "^nameserver $DOCKSAL_IP$" /etc/resolver/$DOCKSAL_DNS_DOMAIN >/dev/null 2>&1); then sudo mkdir -p /etc/resolver # deleting old resolver is the only way to get mac to update config sudo rm -r "/etc/resolver/$DOCKSAL_DNS_DOMAIN" >/dev/null 2>&1 # NO \n at the beginning of line here! echo -e "# .$DOCKSAL_DNS_DOMAIN domain resolution\nnameserver $DOCKSAL_IP" | \ sudo tee 1>/dev/null "/etc/resolver/$DOCKSAL_DNS_DOMAIN" fi elif [[ "$mode" == "off" ]]; then sudo rm -r "/etc/resolver/$DOCKSAL_DNS_DOMAIN" >/dev/null 2>&1 fi # Flush DNS cache echo-green "Clearing DNS cache..." sudo dscacheutil -flushcache # to check: scutil --dns } # Configure system-wide *.docksal resolver via /etc/resolv.conf on Alpine configure_resolver_alpine () { local mode="${1:-on}" local dns_settings="nameserver ${DOCKSAL_IP}" local conf_file="/etc/resolv.conf" # Enabling and settings are not present if [[ "$mode" == "on" ]] && (! grep -q "$dns_settings" ${conf_file}); then # Inline sed (sed -i) does not work with PWD/DnD. It it deletes the destination file first. # /etc/resolv.conf cannot be deleted in a docker container, so it fails. # We have to write into the file directly. #echo -e "${dns_settings}\n$(cat /etc/resolv.conf)" | sudo tee /etc/resolv.conf # Append dns_settings at the beginning local dns_conf_text=$(sed "1i $dns_settings" ${conf_file}) echo "$dns_conf_text" | sudo tee ${conf_file} >/dev/null # Disabling and settings are present elif [[ "$mode" == "off" ]] && (grep -q "$dns_settings" ${conf_file}); then local dns_conf_text=$(sed "/${dns_settings}/d" ${conf_file}) echo "$dns_conf_text" | sudo tee ${conf_file} >/dev/null fi } # Configure system-wide *.docksal resolver # @param $1 mode, set to "off" to disable/revert settings, leave empty to enable configure_resolver_windows () { local mode="${1:-on}" local dns_ip="$DOCKSAL_IP" # 10 is used increase the adapter's priority. 75 to - deprioritize it local metric=10 # Enable resolver by default if [[ "$mode" == "off" ]]; then dns_ip="none" metric=75 fi local network_id if is_docker_native; then # Use the default DockerNAT adapter network_id="vEthernet (DockerNAT)" else # Figure out the name of the network (not the name of the adapter) the VM is using network_id=$("$vboxmanage" showvminfo ${DOCKER_MACHINE_NAME} --machinereadable | grep hostonlyadapter | cut -d'"' -f2 | sed 's/Ethernet Adapter/Network/') fi if [[ "$network_id" == "" ]]; then echo-error "DNS resolver configuration failed" return 1 fi # Set DNS server to Docksal IP on the VM's VBox adapter local command1="netsh interface ip set dnsservers \"${network_id}\" static ${dns_ip} none" # Set the adapter metric (priority) to 10, so its DNS settings will take over other adapters local command2="netsh interface ip set interface \"${network_id}\" metric=${metric}" winsudo "$command1 && $command2" # Flush DNS cache echo-green "Clearing DNS cache..." ipconfig /flushdns >/dev/null 2>&1 } # Configure system-wide *.docksal resolver # @param $1 mode, set to "off" to disable/revert settings, leave empty to enable configure_resolver_wsl () { local mode="${1:-on}" local dns_ip="$DOCKSAL_IP" # 10 is used increase the adapter's priority. 75 to - deprioritize it local metric=10 # Enable resolver by default if [[ "$mode" == "off" ]]; then dns_ip="none" metric=75 fi local network_id network_id="vEthernet (DockerNAT)" # Set DNS server to Docksal IP on the VM's VBox adapter cmd.exe /c netsh interface ip set dnsservers "${network_id}" static ${dns_ip} none # Set the adapter metric (priority) to 10, so its DNS settings will take over other adapters cmd.exe /c netsh interface ip set interface "${network_id}" metric=${metric} # Flush DNS cache echo-green "Clearing DNS cache..." ipconfig.exe /flushdns >/dev/null 2>&1 } # Configure system-wide *.docksal resolver (Windows is not supported) configure_resolver () { # Do not configure the resolver if asked so [[ "$DOCKSAL_NO_DNS_RESOLVER" != "" ]] && return local mode="${1:-on}" if [[ "$mode" == "on" ]]; then echo-green "Enabling automatic *.$DOCKSAL_DNS_DOMAIN DNS resolver..." detect_dns # The following check requires nslookup. Skip it if the binary is not present. if (which nslookup >/dev/null 2>&1); then # Probing the upstream DNS server # We want to make sure the upstream DNS server is reachable before configuring the host to use our DNS service. # If it's not reachable, hosts DNS resolution may be affected or stop working entirely. # nslookup returns 0 exit code regardless of the results, so have to pipe it to grep nslookup -timeout=1 google.com "$DOCKSAL_DNS_UPSTREAM" 2>/dev/null | grep google.com >/dev/null local ret=$? if [[ "$ret" != "0" ]]; then echo-error "The upstream DNS server ($DOCKSAL_DNS_UPSTREAM) is not reachable." \ "You are either offline, $DOCKSAL_DNS_UPSTREAM is not a valid DNS server, or DNS requests to $DOCKSAL_DNS_UPSTREAM are blocked." \ "Once the current operation completes, follow the steps below:" \ " 1. Open ${yellow}~/.docksal/docksal.env${NC} and set ${yellow}DOCKSAL_DNS_UPSTREAM${NC} to your local network DNS server" \ " 2. Run ${yellow}fin system reset dns${NC} to see if it works" _confirm "Continue?" fi fi elif [[ "$mode" == "off" ]]; then echo-green "Disabling automatic *.$DOCKSAL_DNS_DOMAIN DNS resolver..." fi is_mac && configure_resolver_mac "$mode" && return $? is_linux && configure_resolver_alpine "$mode" && return $? is_windows && configure_resolver_windows "$mode" && return $? is_wsl && configure_resolver_wsl "$mode" && return $? } install_sshagent_service () { docker rm -f docksal-ssh-agent >/dev/null 2>&1 || true docker volume rm docksal_ssh_agent >/dev/null 2>&1 || true docker volume create --name docksal_ssh_agent >/dev/null 2>&1 docker run -d --name docksal-ssh-agent --label "io.docksal.group=system" --restart=always \ --mount type=volume,src=docksal_ssh_agent,dst=/.ssh-agent \ "${IMAGE_SSH_AGENT}" >/dev/null # Add default keys. Using || true here to suppress errors if there are no keys on the host. ssh_add || true } # Install Mac dependencies install_tools_mac () { mkdir -p "$CONFIG_DOWNLOADS_DIR" 2>/dev/null if_failed "Could not create $CONFIG_DOWNLOADS_DIR" # Install Docker client if ! is_docker_version; then local docker_pkg=$(basename "$URL_DOCKER_MAC") echo-green "Installing docker client v${REQUIREMENTS_DOCKER}..." if [[ ! -f "$docker_pkg" ]]; then echo-green "Downloading ${docker_pkg}..." curl -fL# "$URL_DOCKER_MAC" -o "$CONFIG_DOWNLOADS_DIR/$docker_pkg" if_failed "Check internet connection." # Use a local/portable copy if available else echo "Found a local copy of ${docker_pkg}. Using it..." cp -f "$docker_pkg" "$CONFIG_DOWNLOADS_DIR" if_failed "Check file permissions." fi # Run in sub-shell so we don't have to cd back ( cd "$CONFIG_DOWNLOADS_DIR" && \ tar zxf "$docker_pkg" && \ mv "docker/docker" "$CONFIG_BIN_DIR/" && \ chmod +x "$DOCKER_BIN" ) if_failed "Check file permissions." fi # Install docker-compose if ! is_docker_compose_version; then local dc_pkg=$(basename "$URL_DOCKER_COMPOSE_MAC") echo-green "Installing docker-compose v${REQUIREMENTS_DOCKER_COMPOSE}..." if [[ ! -f "$dc_pkg" ]]; then echo-green "Downloading ${dc_pkg}..." curl -fL# "$URL_DOCKER_COMPOSE_MAC" -o "$DOCKER_COMPOSE_BIN" && \ chmod +x "$DOCKER_COMPOSE_BIN" if_failed "Check file permissions and internet connection." # Use a local/portable copy if available else echo "Found a local copy of ${dc_pkg}. Using it..." cp -f "$dc_pkg" "$DOCKER_COMPOSE_BIN" && \ chmod +x "$DOCKER_COMPOSE_BIN" if_failed "Check file permissions." fi fi # Install docker-machine if ! is_docker_machine_version; then local dm_pkg=$(basename "$URL_DOCKER_MACHINE_MAC") echo-green "Installing docker-machine v${REQUIREMENTS_DOCKER_MACHINE}..." if [[ ! -f "$dm_pkg" ]]; then echo-green "Downloading ${dm_pkg}..." curl -fL# "$URL_DOCKER_MACHINE_MAC" -o "$DOCKER_MACHINE_BIN" && \ chmod +x "$DOCKER_MACHINE_BIN" if_failed "Check file permissions and internet connection." # Use a local/portable copy if available else echo "Found a local copy of ${dm_pkg}. Using it..." cp -f "$dm_pkg" "$DOCKER_MACHINE_BIN" && \ chmod +x "$DOCKER_MACHINE_BIN" if_failed "Check file permissions." fi fi # Cleanup rm -rf "$CONFIG_DOWNLOADS_DIR" >/dev/null 2>&1 # If it was an upgrade [[ "$BOOT2DOCKER_NEEDS_AN_UPGRADE" == "1" ]] && \ is_docker_machine_running && \ echo-green "Preparing to update virtual machine..." && \ docker_machine_stop && \ echo "Downloading boot2docker.iso v${REQUIREMENTS_DOCKER}" && \ curl -fL# "$URL_BOOT2DOCKER" -o "$HOME/.docker/machine/machines/$DOCKER_MACHINE_NAME/boot2docker.iso" && \ docker_machine_start if is_docker_native && [[ ${UPGRADE_IN_PROGRESS} == 1 ]]; then local server_version=$(docker version --format '{{.Server.Version}}') if [[ $(ver_to_int "$REQUIREMENTS_DOCKER") > $(ver_to_int "$server_version") ]]; then echo-alert "Update Docker for Mac to the latest version manually" sleep 2 fi fi } # Install Windows dependencies install_tools_windows () { mkdir -p "$CONFIG_DOWNLOADS_DIR" 2>/dev/null if_failed "Could not create $CONFIG_DOWNLOADS_DIR" # Remove old incorrect line sed -i "s/^\\\\nexport.*$//" "$HOME/.babunrc" # Disable Babun update checks as they often bring problems if ! (grep '^export DISABLE_CHECK_ON_STARTUP="true"' "$HOME/.babunrc" >/dev/null 2>&1); then echo -e "\n"'export DISABLE_CHECK_ON_STARTUP="true"' >> "$HOME/.babunrc" fi # Install Docker client if ! is_docker_version; then local docker_pkg=$(basename "$URL_DOCKER_WIN") echo-green "Installing docker client v${REQUIREMENTS_DOCKER}..." if [[ ! -f "$docker_pkg" ]]; then echo-green "Downloading ${docker_pkg}..." curl -fL# "$URL_DOCKER_WIN" -o "$CONFIG_DOWNLOADS_DIR/$docker_pkg" if_failed "Check internet connection." # Use a local/portable copy if available else echo "Found a local copy of ${docker_pkg}. Using it..." cp -f ${docker_pkg} "$CONFIG_DOWNLOADS_DIR" if_failed "Check file permissions." fi # Run in sub-shell so we don't have to cd back ( cd "$CONFIG_DOWNLOADS_DIR" && \ unzip ${docker_pkg} && \ mv "docker/docker.exe" "$DOCKER_BIN.exe" && \ chmod +x "$DOCKER_BIN" ) if_failed "Check file permissions." fi # Install docker-compose if ! is_docker_compose_version; then local dc_pkg=$(basename "$URL_DOCKER_COMPOSE_WIN") echo-green "Installing docker-compose v${REQUIREMENTS_DOCKER_COMPOSE}..." if [[ ! -f "$dc_pkg" ]]; then echo-green "Downloading ${dc_pkg}..." curl -fL# "$URL_DOCKER_COMPOSE_WIN" -o "$DOCKER_COMPOSE_BIN.exe" && \ chmod +x "$DOCKER_COMPOSE_BIN.exe" if_failed "Check file permissions and internet connection." # Use a local/portable copy if available else echo "Found a local copy of ${dc_pkg}. Using it..." cp -f "$dc_pkg" "$DOCKER_COMPOSE_BIN.exe" && \ chmod +x "$DOCKER_COMPOSE_BIN.exe" if_failed "Check file permissions." fi fi # Install docker-machine if ! is_docker_machine_version; then local dm_pkg=$(basename "$URL_DOCKER_MACHINE_WIN") echo-green "Installing docker-machine v${REQUIREMENTS_DOCKER_MACHINE}..." if [[ ! -f "$dm_pkg" ]]; then echo-green "Downloading ${dm_pkg}..." curl -fL# "$URL_DOCKER_MACHINE_WIN" -o "$DOCKER_MACHINE_BIN.exe" && \ chmod +x "$DOCKER_MACHINE_BIN.exe" if_failed "Check file permissions and internet connection." # Use a local/portable copy if available else echo "Found a local copy of ${dm_pkg}. Using it..." cp -f "$dm_pkg" "$DOCKER_MACHINE_BIN.exe" && \ chmod +x "$DOCKER_MACHINE_BIN.exe" if_failed "Check file permissions." fi fi # Install winpty if ! is_winpty_version; then echo-green "Installing winpty v${REQUIREMENTS_WINPTY}..." local winpty_pkg=$(basename "$URL_WINPTY") if [[ ! -f "$winpty_pkg" ]]; then echo-green "Downloading ${winpty_pkg}..." curl -fL# "$URL_WINPTY" -o "$CONFIG_DOWNLOADS_DIR/$winpty_pkg" if_failed "Check internet connection." # Use a local/portable copy if available else echo "Found a local copy of ${winpty_pkg}. Using it..." cp -f "$winpty_pkg" "$CONFIG_DOWNLOADS_DIR" if_failed "Check file permissions." fi # Run in sub-shell so we don't have to cd back ( cd "$CONFIG_DOWNLOADS_DIR" && \ tar zxf "$winpty_pkg" && \ mv winpty-*/bin/* "$CONFIG_BIN_DIR/" ) if_failed "Check file permissions." fi # Cleanup rm -rf "$CONFIG_DOWNLOADS_DIR" >/dev/null 2>&1 # Run one-time adjustments during install only. if [[ ${UPGRADE_IN_PROGRESS} != 1 ]]; then # Optimize SMB sharing smb_windows_fix [ ! $? -eq 0 ] && echo-red "Failed. Windows shares might not work properly with Docksal." # Git settings echo-green "Adjusting git defaults..." echo "git config --global core.autocrlf input" git config --global core.autocrlf input # do not convert line breaks. treat as is echo "git config --system core.longpaths true" git config --system core.longpaths true # support long paths fi if ! is_docker_native && [[ "$BOOT2DOCKER_NEEDS_AN_UPGRADE" == "1" ]]; then is_docker_machine_running && \ echo-green "Preparing to update virtual machine..." && \ docker_machine_stop && \ echo "Downloading boot2docker.iso v${REQUIREMENTS_DOCKER}" && \ curl -fL# "$URL_BOOT2DOCKER" -o "$(cygpath $USERPROFILE)/.docker/machine/machines/$DOCKER_MACHINE_NAME/boot2docker.iso" && \ docker_machine_start fi if is_docker_native && [[ ${UPGRADE_IN_PROGRESS} == 1 ]]; then local server_version=$(docker version --format '{{.Server.Version}}') if [[ $(ver_to_int "$REQUIREMENTS_DOCKER") > $(ver_to_int "$server_version") ]]; then echo-alert "Update Docker for Windows to the latest version manually" sleep 2 fi fi } # Install tools for Debian install_tools_debian () { # Install Docker client sudo service docker start 2>/dev/null if ! is_docker_version || ! is_docker_server_version; then echo-green "Installing Docker..." # Stop docker service if it exists if ps aux | grep dockerd | grep -v grep >/dev/null 2>&1; then echo "Stopping docker service..." sudo service docker stop 2>/dev/null fi # Pin docker-engine version to avoid apt-get upgrade. # Get rid of -ce part because linux packages use `~ce` suffix in repos and pin version will not work with it local PIN_DOCKER_VERSION=$(echo ${REQUIREMENTS_DOCKER} | sed "s/-.*//") echo -e "Package: docker-ce\nPin: version ${PIN_DOCKER_VERSION}*\nPin-Priority: 1001" | sudo tee "/etc/apt/preferences.d/docksal.pref" >/dev/null || exit 1 curl -fsSL "${URL_DOCKER_NIX}" | \ # Patch get.docker.com script on the fly to force installation of the pinned docker-ce version sed "s/--force-yes docker-ce >/docker-ce >/" | \ sh && \ # Using $LOGNAME, not $(whoami). See http://stackoverflow.com/a/4598126/4550880. sudo usermod -aG docker "$LOGNAME" sudo service docker start 2>/dev/null sudo docker version if_failed "Docker installation/upgrade has failed." fi # Install Docker Compose if ! is_docker_compose_version; then echo-green "Installing Docker Compose..." sudo curl -fL# "$URL_DOCKER_COMPOSE_NIX" -o "$DOCKER_COMPOSE_BIN_NIX" && \ sudo chmod +x "$DOCKER_COMPOSE_BIN_NIX" && \ docker-compose --version if_failed "Docker Compose installation/upgrade has failed." fi } # Install tools for Fedora install_tools_fedora () { # Install Docker client sudo service docker start 2>/dev/null if ! is_docker_version || ! is_docker_server_version; then echo-green "Installing Docker..." # Stop docker service if it exists if ps aux | grep dockerd | grep -v grep >/dev/null 2>&1; then echo "Stopping docker service..." sudo service docker stop 2>/dev/null fi # Install stable Docker version curl -fsSL "${URL_DOCKER_NIX}" | CHANNEL=stable sh if_failed "Docker installation/upgrade has failed." # Have to create group here to add user to it sudo groupadd docker sudo usermod -aG docker ${USER} sudo service docker start 2>/dev/null sudo docker version fi # Install Docker Compose if ! is_docker_compose_version; then echo-green "Installing Docker Compose..." sudo curl -fL# "$URL_DOCKER_COMPOSE_NIX" -o "$DOCKER_COMPOSE_BIN_NIX" && \ sudo chmod +x "$DOCKER_COMPOSE_BIN_NIX" && \ docker-compose --version if_failed "Docker Compose installation/upgrade has failed." fi } # Install Alpine dependencies install_tools_alpine () { # Install Docker # TODO: automate this if ! is_docker_version || ! is_docker_server_version; then echo-error "Docker version should be ${REQUIREMENTS_DOCKER} or greater." \ "Automated Docker installation is not supported on this platform." \ "Please install Docker ${REQUIREMENTS_DOCKER} (or greater) manually, then try again." exit 1 fi # Install Docker Compose if ! is_docker_compose_version; then echo-green "Installing Docker Compose..." # docker-compose has to be install via pip on Alpine # See https://github.com/docker/compose/issues/3465 apk add --no-cache python3 >/dev/null && \ pip3 install "docker-compose==${REQUIREMENTS_DOCKER_COMPOSE}" >/dev/null && \ docker-compose --version if_failed "Docker Compose installation/upgrade has failed." fi } # Install Alpine dependencies install_tools_generic_linux () { # Require Docker if ! is_docker_version || ! is_docker_server_version; then echo-error "Docker version should be ${REQUIREMENTS_DOCKER} or greater." \ "Automated Docker installation is not supported on this platform." \ "Install Docker ${REQUIREMENTS_DOCKER} or greater manually and run Docksal installation again." exit 1 fi # Install Docker Compose if ! is_docker_compose_version; then echo-green "Installing Docker Compose..." sudo curl -fL# "$URL_DOCKER_COMPOSE_NIX" -o "$DOCKER_COMPOSE_BIN_NIX" && \ sudo chmod +x "$DOCKER_COMPOSE_BIN_NIX" && \ docker-compose --version if_failed "Docker Compose installation/upgrade has failed." fi } # Install WSL dependencies install_tools_wsl () { # Install Docker client if ! is_docker_version; then echo-green "Installing Docker..." # Install packages to allow apt to use a repository over HTTPS sudo apt-get install apt-transport-https ca-certificates curl software-properties-common # Add Docker's official GPG key curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - # Set up the repository sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" # Update source lists sudo apt-get update # Install Docker sudo apt-get install docker-ce -y --allow-unauthenticated if_failed "Docker installation/upgrade has failed." fi # Install Docker Compose if ! is_docker_compose_version; then echo-green "Installing Docker Compose..." sudo curl -fL# "$URL_DOCKER_COMPOSE_NIX" -o "$DOCKER_COMPOSE_BIN_NIX" && \ sudo chmod +x "$DOCKER_COMPOSE_BIN_NIX" && \ docker-compose --version if_failed "Docker Compose installation/upgrade has failed." fi system_reset } update_virtualbox () { local url local vbox_pkg # Preparation if is_windows; then url="$URL_VBOX_WIN" vbox_pkg=$(cygpath -u "$USERPROFILE/Downloads")/$(basename "$url") fi if is_mac; then url="$URL_VBOX_MAC" vbox_pkg="$HOME/Downloads"/$(basename "$url") fi # Prefer the package found in the current directory if [[ -f $(basename "$url") ]]; then vbox_pkg="./"$(basename "$url") fi # Check again that file exists either in the current folder or in Downloads if [[ -f "$vbox_pkg" ]]; then echo "Found VirtualBox $REQUIREMENTS_VBOX in '$vbox_pkg'. Using it..." else if is_tty; then _confirm "Do you want to download VirtualBox $REQUIREMENTS_VBOX to '$vbox_pkg' and install it?" else echo-green "Downloading and installing VirtualBox $REQUIREMENTS_VBOX" fi # curl with download the package to the Downloads location curl -fL# "$url" -o "$vbox_pkg" fi # Stop VM fin vm stop >/dev/null 2>&1 # Download and install Mac is_mac && ( echo "Attaching ${vbox_pkg}" hdiutil detach "/Volumes/VirtualBox" >/dev/null 2>&1 hdiutil attach "$vbox_pkg" -quiet && open "/Volumes/VirtualBox" && sleep 1 && open "/Volumes/VirtualBox/VirtualBox.pkg" ) # Download and install Win is_windows && ( chmod +x "$vbox_pkg" "$vbox_pkg" ) # Wait for installation to finish echo if ! is_tty; then echo "Please finish VirtualBox installation using installer (Ctrl+C to exit)..." fi while ! is_vbox_version; do read -p "Please finish VirtualBox installation and press ENTER to continue (Ctrl+C to exit)..." sleep 1 done } update_tools () { if is_linux; then is_debian && install_tools_debian && return $? is_fedora && install_tools_fedora && return $? is_alpine && install_tools_alpine && return $? # All other distros install_tools_generic_linux && return $? fi is_wsl && install_tools_wsl && return $? is_windows && install_tools_windows && return $? is_mac && install_tools_mac && return $? } update_config_files () { files_to_download=" $URL_STACKS_OVERRIDES_IDE $URL_STACKS_OVERRIDES_OSXFS $URL_STACKS_SERVICES $URL_STACKS_STACK_ACQUIA $URL_STACKS_STACK_DEFAULT $URL_STACKS_STACK_DEFAULT_NODB $URL_STACKS_STACK_NODE $URL_STACKS_VOLUMES_BIND $URL_STACKS_VOLUMES_NFS $URL_STACKS_VOLUMES_UNISON " ( cd "$CONFIG_STACKS_DIR" || exit 1; # Cleanup the stacks directory before downloading files (this will help with updates, when file names are changing). rm -f "$CONFIG_STACKS_DIR/*" for f in ${files_to_download}; do echo "$(basename ${f})" # exit subshell with error if download failed curl -kfsSLO "$f" || exit 1 done ) if_failed_error "One of the stack files was not downloaded." \ "Check your internet connection and file permission on $CONFIG_STACKS_DIR" } # Install shell commands autocomplete script install_bash_autocomplete () { local destination="$FIN_AUTOCOMPLETE_PATH" tee "$destination" >/dev/null <<'EOF' _docksal_completion() { local cur=${COMP_WORDS[COMP_CWORD]} #current word part local prev=${COMP_WORDS[COMP_CWORD-1]} #previous word local compwords=$(fin bash_comp_words $prev) #get completions for previous word if [ ! $? -eq 0 ]; then return 1; else COMPREPLY=( $(compgen -W "$compwords" -- $cur) ) return 0 fi } complete -o bashdefault -o default -F _docksal_completion fin EOF if_failed "Failed to write file to $destination" echo-green "Script saved to $destination" chmod +x "$destination" SOURCE_FILE=".bash_profile" grep -q "$destination" "$HOME/$SOURCE_FILE" if [[ $? -ne 0 ]]; then echo -e ". $destination" >> "$HOME/$SOURCE_FILE" if_failed "Failed to write file to $HOME/$SOURCE_FILE" echo-green "Autocomplete appended to $HOME/$SOURCE_FILE" echo-yellow "Please restart your bash session to apply" fi } # Update system service images update_system_images () { check_docker_running IMAGE_FILE=${IMAGE_FILE:-docksal-system-images.tar} if [[ -f "$IMAGE_FILE" ]]; then # Load system images from the current directory when available echo "Found $IMAGE_FILE. Using it..." image_load "$IMAGE_FILE" else # Load system images as usually from Docker Hub docker pull "${IMAGE_VHOST_PROXY}" docker pull "${IMAGE_DNS}" docker pull "${IMAGE_SSH_AGENT}" fi system_reset } # Update project images update_project_images () { # Need docker-compose configuration to be properly loaded here load_configuration check_docksal_environment docker-compose pull # update project containers images up } # Update Docksal update () { # If running from $FIN_PATH_UPDATED then it is fin.updated run by old fin and we need to skip some steps [[ "$0" == "$FIN_PATH_UPDATED" ]] && FULL_UPDATE="exit 1" # Pre-update check #1: check if there is existing but stopped Docksal machine # We need it running to know Docker server version if (${FULL_UPDATE}); then if ! is_linux && ! is_docker_native && ! is_wsl; then # Check VirtualBox is_vbox_version local res=$? if [[ "$res" != "0" ]]; then [[ "$res" == "2" ]] && echo-red "VirtualBox version should be $REQUIREMENTS_VBOX or higher" (which 'docker-machine' >/dev/null 2>&1) && vm stop DOCKER_MACHINE_STATUS="Stopped" update_virtualbox (which 'docker-machine' >/dev/null 2>&1) && vm start fi fi if ! is_linux && ! is_docker_native && ! is_wsl && which 'docker-machine' >/dev/null 2>&1; then if is_docker_machine_exist && ! is_docker_machine_running; then echo-error "Docker Machine '$DOCKER_MACHINE_NAME' should be running to perform update" \ "Start it with ${yellow}fin vm start${NC} or destroy it with ${yellow}fin vm remove${NC} if you want it re-created." return 1 fi fi if is_wsl; then # Check Docker for Win running IFS=':' read nc_host nc_port <<< "$DOCKER_HOST" nc -w 1 ${nc_host} ${nc_port} if_failed_error "Docker for Windows is not running" "Start Docker for Windows, wait for Docker to start and run update again." # Check for administrative privileges if needed if ! (ipconfig.exe | grep "$DOCKSAL_IP") || ! (ipconfig.exe | grep "$DOCKSAL_HOST_IP"); then cd "/mnt/c" touch "file.tmp" if [[ "$?" != "0" ]]; then echo-error "Docksal needs administrative privileges to set up network preferences." \ "--" \ "1. Close this bash window (you can't run elevated bash while this one is running)" \ "2. Run cmd.exe as an Administrator" \ "3. Start bash in that elevated cmd.exe session" \ "4. Run fin update in that elevated bash session" # It is not allowed to start elevated and un-elevated bash simultaneously exit 1 fi rm -f "file.tmp" fi fi fi # Checking docker status if (which 'docker' >/dev/null 2>&1); then is_docker_running RUNNING_CODE=$? # cache status if (exit ${RUNNING_CODE}); then # If docksal services present, then we are doing an upgrade. is_docksal_running && UPGRADE_IN_PROGRESS=1 else # Check out the case when current user on Ubuntu does not have access to docker daemon if is_linux; then ps -p $(cat /var/run/docker.pid) 2>&1 >/dev/null [ ! $? -eq 0 ] && LINUX_DAEMON_RUNNING="exit 1" # if daemon is running but docker info does not return 0 then this is the case if (${LINUX_DAEMON_RUNNING}) && ! (exit ${RUNNING_CODE}); then echo-red "Your Docker daemon is running but you don't have access to it." echo-yellow "Please run 'newgrp docker' or reboot." exit 1 fi fi fi fi # Pre-update check #2: warn if boot2docker will need to be upgraded if (is_mac || is_windows) && ! is_docker_native && [[ ${UPGRADE_IN_PROGRESS} == 1 ]]; then local b2d_version=$(docker version --format '{{.Server.Version}}') if [[ $(ver_to_int "$REQUIREMENTS_DOCKER") > $(ver_to_int "$b2d_version") ]]; then BOOT2DOCKER_NEEDS_AN_UPGRADE=1 fi fi # [STEP 0] Define total steps. Upgrade has 2 more steps if [[ ${UPGRADE_IN_PROGRESS} == 1 ]]; then TOTAL_STEPS=6 else TOTAL_STEPS=4 fi # [STEP 1] Update fin unless already running fin.updated binary if (${FULL_UPDATE}); then testing_warn echo-green "[STEP 1/$TOTAL_STEPS] Updating fin..." local fin_url=$(get_fin_url) local new_fin new_fin=$(curl -kfsSL "${fin_url}?r=$RANDOM") if_failed_error "fin download failed." # Check if fin update is required and whether it is a major version local new_version=$(echo "$new_fin" | grep "^FIN_VERSION=" | cut -f 2 -d "=") if [[ "$new_version" != "$FIN_VERSION" ]]; then local current_major_version=$(echo "$FIN_VERSION" | cut -d "." -f 1) local new_major_version=$(echo "$new_version" | cut -d "." -f 1) if [[ "$current_major_version" != "$new_major_version" ]]; then echo -e "${red_bg} WARNING ${NC} ${red}Non-backwards compatible version update${NC}" echo -e "Updating from ${yellow}$FIN_VERSION${NC} to ${yellow}$new_version${NC} is not backward compatible." echo "You may not be able to use you current Docksal environment if you proceed." echo -e "Please read update documentation: ${yellow}$URL_REPO_UI#updates${NC}" _confirm "Continue with the update?" fi # saving to file echo "$new_fin" | sudo tee "$FIN_PATH_UPDATED" > /dev/null if_failed_error "Could not write $FIN_PATH_UPDATED" sudo chmod +x "$FIN_PATH_UPDATED" local new_version=$(${FIN_PATH_UPDATED} v) echo "fin $new_version downloaded..." # Run other Updates 2-5 with newly downloaded fin version ( "$FIN_PATH_UPDATED" update ) # overwrite old fin sudo mv "$FIN_PATH_UPDATED" "$FIN_PATH" exit else echo "Already at $FIN_VERSION." fi fi # [STEP 2] Update stacks echo-green "[STEP 2/$TOTAL_STEPS] Updating stack files..." update_config_files # [STEP 3] Update third party tools echo-green "[STEP 3/$TOTAL_STEPS] Updating tools..." update_tools # [STEP 4] Update system images echo-green "[STEP 4/$TOTAL_STEPS] Updating system images..." if [[ ${UPGRADE_IN_PROGRESS} == 1 ]]; then # [4.1] Always update system images during upgrade # On Win and Mac update only during upgrade because it is done in docker_machine_start update_system_images # Run cleanup to get rid of outdated images cleanup else # [4.2] On Linux update during install, but use sudo as user will not be in docker group yet. if is_linux; then # Override docker to run it via sudo docker () { sudo "$(which docker)" "$@" } # Install/update Docksal system services update_system_images fi # [4.3] With native mode update during install, but no cleanup if is_docker_native; then # Install/update Docksal system services update_system_images fi fi # [STEP 5] Update project images if [[ ${UPGRADE_IN_PROGRESS} == 1 ]]; then echo-green "[STEP 5/$TOTAL_STEPS] Updating Docksal images" for _image in $(docker images --format '{{.Repository}}:{{.Tag}}' | grep docksal | sort | uniq); do docker pull "$_image" done fi # [STEP 6] Restart running projects if BOOT2DOCKER was not restarted # This is needed to re-append restarted vhost-proxy to containers networks if [[ ${UPGRADE_IN_PROGRESS} == 1 ]] && [[ "$BOOT2DOCKER_NEEDS_AN_UPGRADE" != "1" ]]; then echo-green "[STEP 6/$TOTAL_STEPS] Restarting running projects..." running_projects=$(docker ps \ --filter 'label=io.docksal.project-root' \ --format '{{.Label "io.docksal.project-root"}}' ) for project in ${running_projects}; do # use updated version when updated [[ ! -f "$FIN_PATH_UPDATED" ]] && FIN_PATH_UPDATED="fin" (cd "$project" && NO_UPDATES=1 "$FIN_PATH_UPDATED" start) done fi # [STEP - POST UPDATE] [[ "$?" == "0" ]] && echo-green "Update finished" if is_linux && ! is_root && [[ ${UPGRADE_IN_PROGRESS} != 1 ]]; then echo-alert "Re-login to enable permanent Docker support." \ "Run ${yellow}newgrp docker${NC} to enable Docker support for current terminal session only." fi } # Return latest fin download url for the latest tag get_latest_fin_url () { # Get the latest release tag URL_REPO_RELEASE="${GITHUB_API}/repos/docksal/docksal/releases/latest" local latest_tag=$(curl -sSL $URL_REPO_RELEASE | grep tag_name | sed 's/\"tag_name": \"\(.*\)",/\1/' | tr -d '[:space:]') # Return the fin raw URL associated with that tag echo "${URL_REPO}/${latest_tag}/bin/fin" } # Return the url for fin. If person has set DOCKSAL_USE_RC then we return that otherwise return masters URL # which will return the latest stable version. get_fin_url () { wants_rc_version && echo $(get_latest_fin_url) || echo "${URL_FIN}" } check_for_updates () { # Never trigger in scripts if ! is_tty; then return; fi local UPDATE_AVAILABLE=0 local UPDATES_AVAILABLE=0 local timestamp; local last_check; local next_check; local last_ping; local next_ping local one_hour=$((60 * 60)) local one_day=$(($one_hour * 24)) local one_week=$(($one_day * 7)) timestamp=$(date +%s) # Set last_check/last_ping to 0 if empty last_check=$(cat "$CONFIG_LAST_CHECK" 2>/dev/null || echo 0) last_ping=$(cat "$CONFIG_LAST_PING" 2>/dev/null || echo 0) # Send ping hourly next_ping=$(( $last_ping + $one_hour )) if [ ${timestamp} -gt ${next_ping} ]; then stats_ping echo "$timestamp" > "$CONFIG_LAST_PING" fi if wants_rc_version; then # Check once a day if DOCKSAL_USE_RC is set next_check=$(( $last_check + $one_day )) else # Check bi-weekly otherwise next_check=$(( $last_check + ($one_week * 2) )) fi if [ ${timestamp} -le ${next_check} ]; then return; fi local new_fin; local new_version # Always write current timestamp to last check file echo "$timestamp" > "$CONFIG_LAST_CHECK" # Get the version of fin that should be used. local fin_url=$(get_fin_url) # No -S for curl here to be completely silent. Connection timeout 1 sec, total max time 3 sec or fail new_fin=$(curl -kfsL --connect-timeout 1 --max-time 3 "${fin_url}?r=${RANDOM}") new_version=$(echo "$new_fin" | grep "^FIN_VERSION=" | cut -f 2 -d "=") if [[ $(ver_to_int "$new_version") > $(ver_to_int ${FIN_VERSION}) ]]; then UPDATE_AVAILABLE=1 echo-green-bg " UPDATE AVAILABLE " echo -e "${green}fin${NC} [ $FIN_VERSION --> $new_version ]" echo "You can update after this process by running fin update" echo "Press Enter to continue" read -p '' fi } # Export docker images from the host into a tar archive # @param $1 mode: --system, --project, --all image_save () { local mode="$1"; shift if [[ "$mode" == "--system" ]]; then echo "Saving system images..." docker ps --filter "label=io.docksal.group=system" --format "{{.Image}}" | xargs docker save -o docksal-system-images.tar elif [[ "$mode" == "--project" ]]; then load_configuration echo "Saving ${COMPOSE_PROJECT_NAME} project images..." docker-compose config | grep image | sed 's/.*image: \(.*\)/\1/' | xargs docker save -o docksal-${COMPOSE_PROJECT_NAME}-images.tar elif [[ "$mode" == "--all" ]]; then echo "Saving all images available on the host..." docker image ls -q | xargs docker save -o docksal-all-images.tar else echo "Usage: save (--system, --project, --all)" fi } # Import docker images from a tar archive # @param $1 file image_load () { local file="$1"; shift docker load -i "$file" } # Show a list of Docksal images or a list of tags for a certain Docksal's Docker hub image # @param $1 image name image_registry_list () { if [[ "$1" != "" ]]; then tag_escaped=${1//\//\\\/} # for instance $1 == "docksal/db" curl -ksSL https://registry.hub.docker.com/v2/repositories/${1}/tags | \ grep -o 'name\":\ \"[-_\.a-zA-Z0-9]*' | \ cut -d " " -f2 | \ tr -d \" | \ sed "s/^/${tag_escaped}:/" else fin docker search "docksal" | grep "^docksal\/" fi } #-------------------------- Execution commands ----------------------------- # Start an interactive bash session in a container # @param $1 container name _bash () { check_docker_running # Interactive shell requires a tty. # On Windows we assume we run interactively via winpty. if ! is_tty; then echo "not a tty" return 1 fi # Pass container name to _run CONTAINER_NAME=$1 _exec bash -i } # Run a command in the cli container changing dir to the same folder # @param $* command with its params to run _exec () { [[ $1 == "" ]] && show_help_exec && exit check_project_environment check_winpty_found eval $(parse_params "$@") # Allow disabling TTY mode. # Useful for non-interactive commands when output is saved into a variable for further comparison. # In a TTY mode the output may contain unexpected control symbols/etc. [[ $T == "T" ]] && local no_tty=true && shift # TODO: refactor to use ${COMPOSE_PROJECT_NAME_SAFE}__1 # CONTAINER_NAME can be used to override where to run. Used in _bash() [[ -n "$in" ]] && CONTAINER_NAME=${in} && shift CONTAINER_NAME=${CONTAINER_NAME:-cli} container_id=$(get_container_id "$CONTAINER_NAME") # ------------------------------------------------ # # 1) cmd local cmd local cdir # Only chdir to the same dir in cli container # RUN_NO_CDIR can be used to override this (used in mysql_import) if [[ "$CONTAINER_NAME" == "cli" ]] && [[ "$RUN_NO_CDIR" != 1 ]]; then local path=$(get_current_relative_path) if [[ "$path" != "" ]] ; then # We are deeper than project root and thus need to do a cd cdir="cd $path &&" fi fi cmd="$cdir" # ------------------------------------------------ # # ------------------------------------------------ # # 2) convert array of parameters into escaped string # Escape spaces that are "spaces" and not parameter delimiters (i.e. param1 param2\ with\ spaces param3) if [[ $2 != "" ]]; then cmd="$cmd "$(printf " %q" "$@") # Do not escape spaces if there is only one parameter (e.g., fin run "ls -la | grep txt") else cmd="$cmd $@" fi # ------------------------------------------------ # # ------------------------------------------------ # # 3) execute # Allow entering arbitrary containers by name (e.g., system containers like vhost-proxy). if [[ "$container_id" == "" ]]; then ${winpty} docker exec -it "$CONTAINER_NAME" sh -i return fi if [[ "$CONTAINER_NAME" == "cli" ]]; then # Commands in cli should be run using the docker user, not root. local container_user='-u docker' fi # Enter project containers # Use the docker user in cli (-u docker) instead of root (default user). # COLUMNS and LINES have to be passed to workaround a bug in Docker. See https://github.com/moby/moby/issues/35407#issuecomment-355753176 if is_tty && [[ "$no_tty" != true ]]; then # interactive # (exit \$?) is a hack to return correct exit codes when docker exec is run with tty (-t). ${winpty} docker exec -e COLUMNS -e LINES -it ${container_user} ${container_id} bash -ilc "$cmd; (exit \$?)" else # non-interactive docker exec -e COLUMNS -e LINES ${container_user} ${container_id} bash -lc "$cmd" fi # ------------------------------------------------ # } # Run a command in a standalone cli container (outside of any project). # The current directory on the host is mapped to /var/www inside the container. # @param $* command with its params to run. run_cli () { check_winpty_found local env_str="" while [ $# -gt 0 ] do case $1 in # Allow disabling TTY mode. # Useful for non-interactive commands when output is saved into a variable for further comparison. # In a TTY mode the output may contain unexpected invisible control symbols/etc. -T) local no_tty=true; shift; ;; # Throw all environment variables into an array so that they can be spit into the docker run command. -e) IFS='=' read -r key value <<< "${2}" # If value is empty set the value to whatever the variable name in $key is # If key does not exist then sets it to empty variable if [[ -z $value ]]; then value="${!key}" fi # Set the $key back to whatever $value is eval export ${key}=`echo -ne \""${value}"\"` # Add only key to $env_str to be used at runtime. env_str="${env_str}-e ${key} " shift 2 ;; --image=*) IFS='=' read -r key image <<< "${1}" shift ;; --clean) # Run without a persistent named volume RUN_CLEAN=1 shift ;; --cleanup) # Re-create persistent named volume RUN_CLEANUP=1 shift ;; --debug) export DEBUG=1 shift ;; *) break ;; esac done # Set default image local RUN_IMAGE="${image:-docksal/cli:2.2-php7.1}" local HOME_VOLUME_NAME="docksal_run_cli_home" local cmd="$@" # Assume bash if no command was given [[ "$cmd" == "" ]] && cmd="bash" # Fail for images from v1, these are executing the commands directly and are not supported anymore. if [[ "$RUN_IMAGE" == docksal/cli:1.* ]]; then echo-error "run-cli command does not support the defined image. Please use docksal/cli:2.0 and above." && exit 1 fi # Remove old persistent volume to re-create it if [[ "$RUN_CLEANUP" == "1" ]]; then docker volume rm -f ${HOME_VOLUME_NAME} >/dev/null 2>&1 fi local MOUNT_HOME if [[ "$RUN_CLEAN" != "1" ]]; then docker volume create ${HOME_VOLUME_NAME} >/dev/null 2>&1 # By default mount as a *persistent* named volume MOUNT_HOME="dst=/home/docker,src=$HOME_VOLUME_NAME" else MOUNT_HOME="dst=/home/docker" fi # Allows run-cli to run global addons local MOUNT_DOCKSAL_HOME="type=bind,src=$CONFIG_DIR,dst=$CONFIG_DIR" # Address an edge case when run-cli is run in the home folder (/root) on PWD/Katacoda # See https://github.com/docksal/docksal/issues/661 if [[ "$(pwd)" == "$HOME" ]] && (is_pwd || is_katacoda); then chown 1000:1000 $HOME fi if is_tty && [[ "$no_tty" != true ]]; then # interactive # (exit \$?) is a hack to return correct exit codes when docker exec is run with tty (-t). # COLUMNS and LINES have to be passed to workaround a bug in Docker. See https://github.com/moby/moby/issues/35407#issuecomment-355753176 ${winpty} docker run --rm -it \ --mount type=bind,src=$(pwd),dst=/var/www \ --mount ${MOUNT_HOME} \ --mount "$MOUNT_DOCKSAL_HOME" \ --mount type=volume,src=docksal_ssh_agent,dst=/.ssh-agent,readonly \ -e SECRET_BLACKFIRE_CLIENT_ID \ -e SECRET_BLACKFIRE_CLIENT_TOKEN \ -e SECRET_SSH_PRIVATE_KEY \ -e SECRET_ACAPI_EMAIL \ -e SECRET_ACAPI_KEY \ -e SECRET_TERMINUS_TOKEN \ -e SECRET_PLATFORMSH_CLI_TOKEN \ -e HOST_UID \ -e HOST_GID \ -e COLUMNS \ -e LINES \ -e DEBUG \ ${env_str} ${RUN_IMAGE} bash -ilc "\"$cmd; (exit \$?)\"" else # non-interactive docker run --rm \ --mount type=bind,src=$(pwd),dst=/var/www \ --mount ${MOUNT_HOME} \ --mount "$MOUNT_DOCKSAL_HOME" \ --mount type=volume,src=docksal_ssh_agent,dst=/.ssh-agent,readonly \ -e SECRET_BLACKFIRE_CLIENT_ID \ -e SECRET_BLACKFIRE_CLIENT_TOKEN \ -e SECRET_SSH_PRIVATE_KEY \ -e SECRET_ACAPI_EMAIL \ -e SECRET_ACAPI_KEY \ -e SECRET_TERMINUS_TOKEN \ -e SECRET_PLATFORMSH_CLI_TOKEN \ -e HOST_UID \ -e HOST_GID \ -e COLUMNS \ -e LINES \ -e DEBUG \ ${env_str} ${RUN_IMAGE} bash -lc "\"$cmd\"" fi } # Start interactive mysql shell # --db-user="admin" to override mysql username # --db-password="otherpass" to override mysql password # --db="drupal" to override database (only applies to queries) _mysql () { check_winpty_found check_docksal_environment eval $(parse_params "$@") # Bring in MYSQL env variables from the db container, so that we can use them here container_id="${COMPOSE_PROJECT_NAME_SAFE}_db_1" container_mysql_vars=$(docker exec "$container_id" env | grep "^MYSQL_") eval ${container_mysql_vars} local __dump_user="${dbuser:-root}" # MYSQL_* variable expansion happens here local __dump_password="${dbpassword:-$MYSQL_ROOT_PASSWORD}" local __database="${db:-$MYSQL_DATABASE}" if [[ "${ARGV[*]}" != "" ]]; then ${winpty} docker exec -it "$container_id" mysql -u${__dump_user} -p${__dump_password} ${__database} -e "${ARGV[*]}" else ${winpty} docker exec -it "$container_id" mysql -u${__dump_user} -p${__dump_password} fi } # Show databases list # --db-user="admin" to override mysql username # --db-password="otherpass" to override mysql password mysql_list () { check_project_environment eval $(parse_params "$@") # Bring in MYSQL env variables from the db container, so that we can use them here container_id="${COMPOSE_PROJECT_NAME_SAFE}_db_1" container_mysql_vars=$(docker exec "$container_id" env | grep "^MYSQL_") eval ${container_mysql_vars} local __dump_user="${dbuser:-root}" local __dump_password="${dbpassword:-$MYSQL_ROOT_PASSWORD}" # -N parameter suppresses columns header _RUN_NO_CDIR=1 CONTAINER_NAME="db" \ _exec "echo 'SHOW DATABASES' | mysql -N -u ${__dump_user} -p${__dump_password}" } # Create a database # --db-user="admin" to override mysql username # --db-password="otherpass" to override mysql password # --db-charset="..." to override charset (default is utf8) # --db-collation="..." to override collation (default is utf8_general_ci) mysql_db_create () { check_project_environment eval $(parse_params "$@") # Bring in MYSQL env variables from the db container, so that we can use them here container_id="${COMPOSE_PROJECT_NAME_SAFE}_db_1" container_mysql_vars=$(docker exec "$container_id" env | grep "^MYSQL_") eval ${container_mysql_vars} local __dump_user="${dbuser:-root}" local __dump_password="${dbpassword:-$MYSQL_ROOT_PASSWORD}" local __db="${ARGV[0]}" local __db_charset="${dbcharset:-utf8mb4}" local __db_collation="${dbcollation:-utf8mb4_unicode_ci}" [[ "${__db}" == "" ]] && echo-error "Provide a name for the database to create" && exit 1 # Set db to space since the db does not exist here yet (_mysql --db=" " --db-user="${__dump_user}" --db-password="${__dump_password}" \ "CREATE DATABASE IF NOT EXISTS \`${__db}\` CHARACTER SET ${__db_charset} COLLATE ${__db_collation};") && # GRANT ALL has to happen in a separate query, otherwise we will be hitting a race condition (when db is not yet created). (_mysql --db="${__db}" --db-user="${__dump_user}" --db-password="${__dump_password}" \ "GRANT ALL PRIVILEGES ON \`${__db}\`.* TO \`${MYSQL_USER}\`;") if_failed_error "Database '${__db}' creation failed" echo "Database '${__db}' created" } # Delete a database # --db-user="admin" to override mysql username # --db-password="otherpass" to override mysql password mysql_db_drop () { check_project_environment eval $(parse_params "$@") # Bring in MYSQL env variables from the db container, so that we can use them here container_id="${COMPOSE_PROJECT_NAME_SAFE}_db_1" container_mysql_vars=$(docker exec "$container_id" env | grep "^MYSQL_") eval ${container_mysql_vars} local __dump_user="${dbuser:-root}" local __dump_password="${dbpassword:-$MYSQL_ROOT_PASSWORD}" local __db="${ARGV[0]}" [[ "${__db}" == "" ]] && echo-error "Provide the name of the database to drop." && exit 1 _mysql --db=" " --db-user="${__dump_user}" --db-password="${__dump_password}" \ "DROP DATABASE IF EXISTS \`${__db}\`" if_failed_error "Dropping '${__db}' database failed" echo "Database '${__db}' dropped" } # Truncate db # @params # $1 - database name # --db-user="admin" to override mysql username # --db-password="otherpass" to override mysql password mysql_db_truncate () { check_project_environment eval $(parse_params "$@") # Bring in MYSQL env variables from the db container, so that we can use them here container_id="${COMPOSE_PROJECT_NAME_SAFE}_db_1" container_mysql_vars=$(docker exec "$container_id" env | grep "^MYSQL_") eval ${container_mysql_vars} local __dump_user="${dbuser:-root}" local __dump_password="${dbpassword:-$MYSQL_ROOT_PASSWORD}" local __database="${1:-${MYSQL_DATABASE:-default}}" echo -e "Truncating ${yellow}${__database}${NC} database..." read -r -d '' DBCREATE_COMMAND <<-EOF SELECT DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME INTO @_charset, @_collation FROM information_schema.schemata WHERE schema_name = '${__database}'; SET @CREATE_TEMPLATE = 'CREATE DATABASE \\\`{DBNAME}\\\` CHARACTER SET {DBCHARSET} COLLATE {DBCOLLATION};'; SET @SQL_SCRIPT = REPLACE(@CREATE_TEMPLATE, '{DBNAME}', '${__database}'); SET @SQL_SCRIPT = REPLACE(@SQL_SCRIPT, '{DBCHARSET}', @_charset); SET @SQL_SCRIPT = REPLACE(@SQL_SCRIPT, '{DBCOLLATION}', @_collation); PREPARE DBCREATE FROM @SQL_SCRIPT; DROP DATABASE \\\`${__database}\\\`; EXECUTE DBCREATE; EOF _RUN_NO_CDIR=1 CONTAINER_NAME="db" \ _exec "echo -e \"$DBCREATE_COMMAND\"| mysql -u${__dump_user} -p${__dump_password} ${__database} " } # Truncate db and import from sql dump # @params # $1 - file name # --db="drupal" to override database username # --db-user="admin" to override mysql username # --db-password="otherpass" to override mysql password # --no-truncate to avoid truncate mysql_import () { check_project_environment eval $(parse_params "$@") # Bring in MYSQL env variables from the db container, so that we can use them here container_id="${COMPOSE_PROJECT_NAME_SAFE}_db_1" container_mysql_vars=$(docker exec "$container_id" env | grep "^MYSQL_") eval ${container_mysql_vars} local __dump_user="${dbuser:-root}" local __dump_password="${dbpassword:-$MYSQL_ROOT_PASSWORD}" local __database="${db:-${MYSQL_DATABASE:-default}}" # /dev/fd/0 is the stdin stream for current script # IMPORTANT: don't run "docker exec" with "-i" until value of /dev/fd/0 is used or its value will be lost local __input="${ARGV[0]:-/dev/fd/0}" [[ "${__input}" != "/dev/fd/0" ]] && [[ ! -r "${__input}" ]] && echo-error "Can not read ${__input}" "Please check file path and permissions" && exit 1 local confirm=0 [[ "$force" == "force" ]] && confirm=1 # Use cat to pipe dump content by default local pipe_cmd='cat' # Show DB import progress if requested and pv binary is available if [[ "$progress" == "progress" ]]; then if (which 'pv' >/dev/null 2>&1); then pipe_cmd='pv' else echo-yellow "Cannot display import progress: pv binary is needed, but missing." fi fi #-- 1) RECREATE DATABASE WITH THE SAME CHARSET AND COLLATION if [[ "$notruncate" != "notruncate" ]]; then echo -e "Truncating ${yellow}${__database}${NC} database..." read -r -d '' DBCREATE_COMMAND <<-EOF SELECT DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME INTO @_charset, @_collation FROM information_schema.schemata WHERE schema_name = '${__database}'; SET @CREATE_TEMPLATE = 'CREATE DATABASE \\\`{DBNAME}\\\` CHARACTER SET {DBCHARSET} COLLATE {DBCOLLATION};'; SET @SQL_SCRIPT = REPLACE(@CREATE_TEMPLATE, '{DBNAME}', '${__database}'); SET @SQL_SCRIPT = REPLACE(@SQL_SCRIPT, '{DBCHARSET}', @_charset); SET @SQL_SCRIPT = REPLACE(@SQL_SCRIPT, '{DBCOLLATION}', @_collation); PREPARE DBCREATE FROM @SQL_SCRIPT; DROP DATABASE \\\`${__database}\\\`; EXECUTE DBCREATE; EOF _RUN_NO_CDIR=1 CONTAINER_NAME="db" \ _exec "echo -e \"$DBCREATE_COMMAND\"| mysql -u${__dump_user} -p${__dump_password} ${__database} " if [[ ! $? -eq 0 ]] && (exit ${confirm}); then _confirm "There were errors during truncation. Continue anyways?" fi fi #-- 2) IMPORTING [[ "${__input}" != "/dev/fd/0" ]] && echo -e "Importing ${yellow}$(basename ${__input})${NC}..." || echo "Importing from stdin..." # We can not use _run here because we need to launch docker exec with only -i param # and only mysql command directly (no bash wrapper) so that stdin could be received inside that exec container_id="${COMPOSE_PROJECT_NAME_SAFE}_db_1" __mysql_command=$(docker exec "$container_id" bash -c "echo -u${__dump_user} -p${__dump_password}") __mysql_command=$(echo "${__mysql_command}" | sed -e 's/[^a-zA-Z0-9_-]$//') # "docker exec -i" is required as it creates stdin/stdout streams but does not create tty ${pipe_cmd} "${__input}" | docker exec -i ${container_id} mysql ${__mysql_command} ${__database} # Check if import succeeded or not and print results. if [ $? -eq 0 ]; then echo-green "Done" exit 0 else echo-red "Import failed" exit 1 fi } # Dump mysql database # @params # $1 - file name, if ommitted then stdout # --db="drupal" to override database username # --db-user="admin" to override mysql username # --db-password="otherpass" to override mysql password mysql_dump () { check_project_environment eval $(parse_params "$@") # Bring in MYSQL env variables from the db container, so that we can use them here container_id="${COMPOSE_PROJECT_NAME_SAFE}_db_1" container_mysql_vars=$(docker exec "$container_id" env | grep "^MYSQL_") eval ${container_mysql_vars} local __dump_user="${dbuser:-root}" local __dump_password="${dbpassword:-$MYSQL_ROOT_PASSWORD}" local __database="${db:-${MYSQL_DATABASE:-default}}" local __output="${ARGV[0]}" if [[ "${ARGV[0]}" != "" ]]; then touch "${__output}" if_failed_error "Could not write ${__output}" "Please check your file permissions" echo-green "Exporting..." fi # TODO: refactor to use ${COMPOSE_PROJECT_NAME_SAFE}__1 container_id=$(get_container_id "db") __mysql_command=$(docker exec -i "$container_id" bash -c "echo -u${__dump_user} -p${__dump_password}") if [[ "${ARGV[0]}" == "" ]]; then docker exec -i "$container_id" mysqldump ${__mysql_command} ${__database} else docker exec -i "$container_id" mysqldump ${__mysql_command} ${__database} | tee "${__output}" >/dev/null fi if [[ "${ARGV[0]}" != "" ]]; then [ $? -eq 0 ] && echo "Done." fi } # Download script by URL and execute it # @param $1 url of script. exec_url () { if [[ "$1" != "" ]]; then _confirm "Run script from '$1'?" local script script=$(curl -kfsSL "$1") if_failed "Failed downloading script $1" shift (eval "${script}") else show_help_exec-url fi } # Show logs # @param $* container(s) name logs () { check_docker_running docker-compose logs "$@" } # Share web container using ngrok service ngrok_share () { check_docksal_environment check_winpty_found eval $(parse_params "$@") local network="${COMPOSE_PROJECT_NAME_SAFE}_default" local container_name=${container:-$(docker-compose ps web | grep Up | grep _web_ | cut -d " " -f 1)} if [[ "$container_name" == "" ]]; then echo-error "Could not find running web container in this project" exit 1 fi local ngrok_container_name=${container_name}_ngrok if ( fin docker ps --format '{{.Names}}' | grep ^${ngrok_container_name}$ >/dev/null ); then docker stop ${ngrok_container_name} >/dev/null docker rm ${ngrok_container_name} >/dev/null 2>/dev/null fi # Remove possible VIRTUAL_HOST user extensions like "my.docksal,*.my.docksal" local NGROK_VIRTUAL_HOST=$(echo "$VIRTUAL_HOST" | sed "s/\(,.*\)//") echo -e "Exposing domain ${yellow}${host:-$NGROK_VIRTUAL_HOST}${NC} via ngrok..." sleep 1 # Based on https://github.com/wernight/docker-ngrok # Using pending PR https://github.com/wernight/docker-ngrok/pull/17 can refactor # and use -e variables on the docker container # Modifying entrypoint.sh file to gain more variables. local ARGS="ngrok" protocol=${protocol:-$NGROK_PROTOCOL} port=${port:-${NGROK_PORT:-80}} # Set the protocol. if [[ "$protocol" == "TCP" ]]; then ARGS="$ARGS tcp" else ARGS="$ARGS http" fi NGROK_PORT=${port#tcp://} auth=${auth:-$NGROK_AUTH} # Set the authorization token. if [[ -n "$auth" ]]; then ARGS="$ARGS -authtoken=${auth} " fi hostname=${hostname:-$NGROK_HOSTNAME} subdomain=${subdomain:-$NGROK_SUBDOMAIN} # Set the subdomain or hostname, depending on which is set if [[ -n "$hostname" && -n "$auth" ]]; then ARGS="$ARGS -hostname=${hostname} " elif [[ -n "$subdomain" && -n "$auth" ]]; then ARGS="$ARGS -subdomain=${subdomain} " fi region=${region:-$NGROK_REGION} # Set a custom region if [[ -n "$region" ]]; then ARGS="$ARGS -region=${region} " fi hostheader=${hostheader:-$NGROK_HEADER} if [[ -n "$hostheader" ]]; then ARGS="$ARGS -host-header=${hostheader} " fi username=${username:-$NGROK_USERNAME} password=${password:-$NGROK_PASSWORD} if [[ -n "$username" && -n "$password" ]]; then ARGS="$ARGS -auth=\"${username}:${password}\" " fi if [[ -n "$username" ]] || [[ -n "$password" ]] || [[ -n "$hostname" ]] || [[ -n "$subdomain" ]]; then if [[ -z "$auth" ]]; then echo-yellow "You must specify a username, password, and Ngrok authentication token to use the custom HTTP authentication." echo-yellow "Sign up for an authentication token at https://ngrok.com" exit 1 fi fi debug=${debug:-$NGROK_DEBUG} if [[ -n "$debug" ]]; then ARGS="$ARGS -log stdout" fi local ngrok_conf_path="$(get_project_path_dc)/.docksal/etc/ngrok" local ngrok_conf="${ngrok_conf_path}/ngrok.yml" if [[ -f "$ngrok_conf" ]]; then CONF_ARGS="-config /var/www/.docksal/etc/ngrok/ngrok.yml" fi local local_ngrok_conf="${ngrok_conf_path}/ngrok-${DOCKSAL_ENVIRONMENT}.yml" if [[ -f "$local_ngrok_conf" ]]; then CONF_ARGS="$CONF_ARGS -config /var/www/.docksal/etc/ngrok/ngrok-${DOCKSAL_ENVIRONMENT}.yml" fi if [[ -f "$ngrok_conf" ]] || [[ -f "$local_ngrok_conf" ]]; then ARGS="ngrok start ${CONF_ARGS} -all" # Overwrite settings with a configuration if its available NGROK_VOLUME="-v $(get_project_path_dc)/.docksal:/var/www/.docksal" fi # Add -host-header by default if no arguments are included. if [[ "${ARGS}" == "ngrok http" ]]; then ARGS="${ARGS} -host-header '${NGROK_VIRTUAL_HOST}'" fi ${winpty} docker run --rm ${NGROK_VOLUME} -it \ --publish 4040 \ --net ${network} \ --link ${container_name} \ --name ${ngrok_container_name} \ wernight/ngrok \ sh -c "${ARGS} ${container_name}.${network}:${NGROK_PORT}" } # Print information required for issue diagnostics # --all will print additional information that is hidden by default as it's not usually needed diagnose () { echo "███ PROJECT CONFIGURATION" if [[ "$(get_project_path)" == "" ]] ; then echo "$(pwd) : not a Docksal project folder" echo else load_configuration config_show fi sysinfo "$@" } # Print system debug information # --all will print additional information that is hidden by default as it's not usually needed sysinfo () { eval $(parse_params "$@") echo "███ OS" # OS version echo "${OS_TYPE} ${OS_NAME} ${OS_VERSION}" uname -a echo echo "███ ENVIRONMENT" && # Docker Mode echo -n "MODE : " if is_linux; then echo "Linux Kernel" elif is_docker_native; then is_mac && echo "Docker for Mac" is_windows && echo "Docker for Windows" else echo "VirtualBox VM" fi if ! is_docker_native; then echo "DOCKER_HOST : $DOCKER_HOST" is_mac && echo "DOCKSAL_NFS_PATH : $DOCKSAL_NFS_PATH" if is_mac; then echo "NFS EXPORTS:" cat /etc/exports fi fi echo echo "███ FIN" && version echo echo "███ DOCKER COMPOSE" echo "EXPECTED VERSION: $REQUIREMENTS_DOCKER_COMPOSE" docker-compose version echo echo "███ DOCKER" echo "EXPECTED VERSION: $REQUIREMENTS_DOCKER" echo docker version if [[ "$all" == "all" ]]; then echo echo "███ DOCKER INFO" docker info fi if ! is_linux && ! is_docker_native; then echo echo "███ DOCKER MACHINE" echo "EXPECTED VERSION: $REQUIREMENTS_DOCKER_MACHINE" docker-machine --version echo docker-machine ls fi if is_docker_running; then echo echo "███ DOCKSAL: PROJECTS" project_list echo echo "███ DOCKSAL: VIRTUAL HOSTS" vhosts echo echo "███ DOCKER: RUNNING CONTAINERS" docker ps echo echo "███ DOCKER: NETWORKS" docker network list if [[ "$all" == "all" ]]; then echo echo "███ DOCKER: IMAGES" docker images fi else echo "███ DOCKER: DOCKER IS NOT RUNNING" echo fi if ! is_docker_native && (which "$vboxmanage" >/dev/null 2>&1); then echo echo "███ VIRTUALBOX" echo "EXPECTED VERSION: $REQUIREMENTS_VBOX" "$vboxmanage" --version if [[ "$all" == "all" ]]; then echo echo "███ VIRTUALBOX NETWORK INTERFACES" "$vboxmanage" list hostonlyifs fi fi if ! is_docker_native && is_docker_machine_running; then echo echo "███ DOCKSAL MOUNTS" vm ssh mount | grep '192.168.64' if [[ $? != 0 ]]; then echo-error "Mounts matching 192.168.64* were not found!" fi fi # Diagnostics information echo echo "███ HDD Usage" if is_linux; then df -h # Linux elif ! is_docker_native; then vm hdd # boot2docker elif is_mac; then df -h # mac native else wmic logicaldisk get size,freespace,caption # win native fi if [[ "$all" == "all" ]]; then echo echo "███ RAM Usage" if is_linux; then free # Linux elif ! is_docker_native; then vm ssh free # boot2docker elif is_mac; then vm_stat # mac native else systeminfo | find "Available Physical Memory" # win native fi echo echo "███ Running containers stats" docker stats $(docker ps --format='{{.Names}}') --no-stream fi if [[ "$all" == "all" ]]; then echo "███ Docksal Proxy status" docker exec docksal-vhost-proxy nginx -t fi # Ping stats server stats_ping } #-------------------------- Links / Aliases ----------------------------- # param $1 path # param $2 alias name alias_create () { [[ $# != 2 ]] && echo 'Usage: fin alias ' && exit 1 mkdir -p "$CONFIG_ALIASES" || exit 1 [[ -h "$2" ]] && echo "Alias $2 already exists" && exit 1 [[ -e "$2" ]] && echo "Filename is not available" && exit 1 [[ ! -d "$1" ]] && echo 'Path should be a valid dir' && exit 1 ! is_windows && \ ln -fs $(get_abs_path "$1") "$CONFIG_ALIASES/$2" [[ $? -eq 0 ]] && \ echo "$2 -> $(get_abs_path $1)" } alias_remove () { [[ ! -h "$CONFIG_ALIASES/$1" ]] && echo 'Alias not found' && exit [[ -h "$CONFIG_ALIASES/$1" ]] && rm "$CONFIG_ALIASES/$1" } alias_list () { local list=$(ls -l "$CONFIG_ALIASES" 2>/dev/null | grep -v total | awk '{printf "%-19s %s\n", $9, $11}') [[ "$list" == "" ]] && echo "No aliases found" && exit printf "%-19s %s\n" "NAME" "TARGET DIR" echo "$list" } #------------------------ Project configuration and variables --------------------------- load_global_configuration () { export DOCROOT=${DOCROOT:-docroot} export PROJECT_ROOT="$(get_project_path)" } load_configuration () { check_project_root # Re-load global env file once again, since 'up' resets some variables to force re-reading config set -a; source "$CONFIG_ENV"; set +a # Mac and Linux use ":"" as a separator, Windows uses ";" local SEPARATOR=':'; is_windows && SEPARATOR=';' local env_file="$(get_project_path_dc)/.docksal/docksal.env" local local_env_file="$(get_project_path_dc)/.docksal/docksal-${DOCKSAL_ENVIRONMENT}.env" if [[ -f "$env_file" ]]; then ENV_FILE="$env_file" fix_crlf_warning "$env_file" # Source and allexport variables in the .env file set -a; source "$env_file"; set +a fi # Source local env file if it exist # Allow using this with the pre-configured stacks by not checking docksal.env presence. if [[ -f "$local_env_file" ]]; then ENV_FILE="${ENV_FILE}${SEPARATOR}${local_env_file}" fix_crlf_warning "$local_env_file" # Source and allexport variables in the .env file set -a; source "$local_env_file"; set +a fi # This is used to print the list of included env files in `fin config` export ENV_FILE # Set COMPOSE_FILE unless it has been already set by user if [[ "$COMPOSE_FILE" == "" ]]; then yml_file="$(get_project_path_dc)/.docksal/docksal.yml" # Allow to define the stack file via DOCKSAL_STACK # Set it to "default" if empty and there is no project yml file [[ "$DOCKSAL_STACK" == "" ]] && export DOCKSAL_STACK="" [[ "$DOCKSAL_STACK" == "" ]] && [[ ! -f "$yml_file" ]] && DOCKSAL_STACK='default' stack_yml_file="$(get_config_dir_dc)/stacks/stack-$DOCKSAL_STACK.yml" # Include both the stack and the project yml files if both exist if [[ -f "$stack_yml_file" ]] && [[ -f "$yml_file" ]]; then COMPOSE_FILE="${stack_yml_file}${SEPARATOR}${yml_file}" # Otherwise try including only one that exists else [[ -f "$stack_yml_file" ]] && COMPOSE_FILE="$stack_yml_file" [[ -f "$yml_file" ]] && COMPOSE_FILE="$yml_file" fi # Throw an error if COMPOSE_FILE is empty here if [[ "$COMPOSE_FILE" == "" ]]; then echo-error "No configuration files found." "Expected in $yml_file" exit 1 else # Include docksal-local.yml (if exists) # Allow using this with the pre-configured stacks by not checking docksal.env presence. local local_yml_file="$(get_project_path_dc)/.docksal/docksal-${DOCKSAL_ENVIRONMENT}.yml" [[ -f "$local_yml_file" ]] && COMPOSE_FILE="${COMPOSE_FILE}${SEPARATOR}${local_yml_file}" fi # Include a volumes yml if requested. Use bind mount for volumes by default. DOCKSAL_VOLUMES=${DOCKSAL_VOLUMES:-bind} if [[ "$DOCKSAL_VOLUMES" != "disable" ]]; then volumes_yml_file="$(get_config_dir_dc)/stacks/volumes-$DOCKSAL_VOLUMES.yml" if [[ -f "$volumes_yml_file" ]]; then COMPOSE_FILE="${volumes_yml_file}${SEPARATOR}${COMPOSE_FILE}" else echo-error "Volumes definition not found in ${volumes_yml_file}." \ "Please check that ${yellow}DOCKSAL_VOLUMES${NC} is set properly." \ "You may need to run ${yellow}fin update${NC} to download volume definitions." exit 1 fi fi export DOCKSAL_VOLUMES # Enable osxfs:cached for cli on Docker for Mac overrides_osxfs_file="$(get_config_dir_dc)/stacks/overrides-osxfs.yml" if is_mac && is_docker_native && [[ -f "${overrides_osxfs_file}" ]] && [[ "$DOCKSAL_VOLUMES" == "bind" ]]; then COMPOSE_FILE="${overrides_osxfs_file}${SEPARATOR}${COMPOSE_FILE}" fi # Enable Cloud9 IDE overrides_ide_file="$(get_config_dir_dc)/stacks/overrides-ide.yml" if [[ -f "${overrides_ide_file}" ]] && [[ "$IDE_ENABLED" != "" ]] && [[ "$IDE_ENABLED" != "0" ]]; then COMPOSE_FILE="${COMPOSE_FILE}${SEPARATOR}${overrides_ide_file}" fi fi export COMPOSE_FILE export DOCKSAL_ENVIRONMENT # Set project name if it was not set previously if [[ -d $(get_project_path) ]]; then local project_name=$(basename $(get_project_path) | tr '[:upper:]' '[:lower:]') COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME:-$project_name} COMPOSE_PROJECT_NAME_SAFE=$(echo ${COMPOSE_PROJECT_NAME} | sed 's/[^-_a-z0-9]//g') COMPOSE_PROJECT_VHOST_NAME_SAFE=$(echo ${COMPOSE_PROJECT_NAME} | tr '[:upper:]' '[:lower:]' | sed 's/_/-/g' | sed 's/[^-a-z0-9]//g') export COMPOSE_PROJECT_NAME export COMPOSE_PROJECT_NAME_SAFE export COMPOSE_PROJECT_VHOST_NAME_SAFE fi # Set defaults if is_pwd || is_katacoda; then # Don't include .$DOCKSAL_DNS_DOMAIN in Play-with-Docker or Katacoda to keep the URL shorter. export VIRTUAL_HOST=${VIRTUAL_HOST:-$COMPOSE_PROJECT_VHOST_NAME_SAFE} else export VIRTUAL_HOST=${VIRTUAL_HOST:-$COMPOSE_PROJECT_VHOST_NAME_SAFE.$DOCKSAL_DNS_DOMAIN} fi export DOCROOT=${DOCROOT:-docroot} local OLD_VHOST=${VIRTUAL_HOST} export VIRTUAL_HOST=$(echo ${VIRTUAL_HOST} | tr '[:upper:]' '[:lower:]' | sed 's/_/-/g' | sed 's/[^-a-z0-9\.]//g') # Warn in case Virtual Host was auto-corrected if [[ "$OLD_VHOST" != "$VIRTUAL_HOST" ]]; then echo-warning "The ${yellow_bold}VIRTUAL_HOST${yellow} has been modified from ${yellow_bold}${OLD_VHOST}${yellow} to ${yellow_bold}${VIRTUAL_HOST}${NC} ${yellow}to comply with browser standards." fi # Make project root globally available export DOCKSAL_PATH="$(get_project_path)" export PROJECT_ROOT="$(get_project_path)" is_windows && export PROJECT_ROOT_WIN="$(get_project_path_dc)" # https://docs.docker.com/compose/reference/envvars/#compose_convert_windows_paths export COMPOSE_CONVERT_WINDOWS_PATHS=1 } # Default init command. Initializes default config and starts containers init () { if [[ "$(get_project_path)" != "" ]] ; then echo-alert "The project exists but no init found" \ "Existing Docksal project was found in $(get_project_path)" \ "Create your own \`init\` command to tell fin how to re-initialize your project." \ "See ${yellow}fin help init${NC} for details" return 1 else _confirm "Initialize a project in $(pwd)?" fi config_generate # Running like this forces configuration re-read fin up } config_show () { check_docksal_environment eval $(parse_params "$@") # Mask secrets prior to printing anything if [[ "$showsecrets" != "showsecrets" ]]; then # Any env variable, that starts with the "SECRET_" prefix, is rewritten to only show beginning and end eval 'secrets=(${!SECRET_@})' for secret in "${secrets[@]}" do local secret_value=${!secret} # Define how many characters to show on the beginning and the ned local cut_length; if [[ ${#secret_value} < 10 ]]; then cut_length=3; elif [[ ${#secret_value} < 15 ]]; then cut_length=4; else cut_length=5; fi eval ${secret}="${secret_value:0:${cut_length}}*****${secret_value: -${cut_length}}" done fi echo "---------------------" # echo "COMPOSE_PROJECT_NAME: ${COMPOSE_PROJECT_NAME}" echo "COMPOSE_PROJECT_NAME_SAFE: ${COMPOSE_PROJECT_NAME_SAFE}" # Replace separators with new lines local SEPARATOR=':'; is_windows && SEPARATOR=';' echo -e "COMPOSE_FILE:\n$(echo ${COMPOSE_FILE} | tr ${SEPARATOR} '\n')" [[ "$ENV_FILE" != "" ]] && echo -e "ENV_FILE:\n$(echo ${ENV_FILE} | tr ${SEPARATOR} '\n')" echo echo "PROJECT_ROOT: ${PROJECT_ROOT}" echo "DOCROOT: ${DOCROOT}" echo "VIRTUAL_HOST: ${VIRTUAL_HOST}" echo "VIRTUAL_HOST_ALIASES: *.${VIRTUAL_HOST}" echo "IP: $DOCKSAL_IP" echo "MYSQL:" $(docker-compose port db 3306 2>/dev/null | sed "s/0\.0\.0\.0/$DOCKSAL_IP/") [[ "$1" == "env" ]] && return echo echo "Docker Compose configuration" echo "---------------------" docker-compose config [[ $? != 0 ]] && return 1 echo "---------------------" } # TODO: consider deprecating this, since now fin automatically creates docksal.yml and docksal.env (with default stack) config_generate () { eval $(parse_params "$@") if [[ -f ".docksal/docksal.yml" ]] || [[ -f ".docksal/docksal.env" ]]; then echo-yellow ".docksal/docksal.yml or .docksal/docksal.env already exists" _confirm "Do you want to proceed and overwrite?" fi rm -f ".docksal/docksal.env" >/dev/null rm -f ".docksal/docksal.yml" >/dev/null # Create .docksal directory if it does not exist mkdir -p ".docksal" # Create docksal.env if it does not exist touch ".docksal/docksal.env" && \ echo "DOCKSAL_STACK=${stack:-default}" > ".docksal/docksal.env" # Get docroot option value or use default local DOCROOT="${docroot:-docroot}" # Add DOCROOT to docksal.env file echo "DOCROOT=${DOCROOT}" >> ".docksal/docksal.env" # Create a basic docroot and index.php if not present if [[ ! -d "$DOCROOT" ]] || [[ ! -f "$DOCROOT/index.php" ]]; then # Setup docroot and a basic index.php mkdir -p "$DOCROOT" && echo ' "$DOCROOT/index.php" fi if [[ $? == 0 ]]; then echo-green "Configuration was generated. You can start it with ${yellow}fin project start${NC}" else echo-error "Something went wrong. Check error messages above." fi } config_get () { eval $(parse_params "$@") # If --global is passed set in global docksal.env local target_env_file if [[ "$global" == "global" ]]; then target_env_file="$CONFIG_ENV" else if [[ "$env" == "" ]]; then target_env_file="$(get_project_path_dc)/.docksal/docksal.env" else target_env_file="$(get_project_path_dc)/.docksal/docksal-${env}.env" fi fi grep "^${ARGV[0]}\=" "$target_env_file" | sed "s/^${ARGV[0]}=\(.*\)$/\1/" } config_set () { eval $(parse_params "$@") # If --global is passed set in global docksal.env local target_env_file if [[ "$global" == "global" ]]; then target_env_file="$CONFIG_ENV" else if [[ "$env" == "" ]]; then target_env_file="$(get_project_path_dc)/.docksal/docksal.env" else target_env_file="$(get_project_path_dc)/.docksal/docksal-${env}.env" fi if [[ ! -f "$target_env_file" ]]; then touch "$target_env_file" if_failed_error "Could not create $target_env_file" fi fi # Warn if no values were found if [[ "${#ARGN[@]}" == 0 ]]; then echo "No values to set" return 1 fi # Loop named params array for param in "${ARGN[@]}" do shift if [[ "${param}" =~ ^- ]]; then continue; fi # Parse variable to set IFS='=' read -r key value <<< "${param}" if grep -q "^$key\=" "$target_env_file"; then # See https://stackoverflow.com/a/29613573 on why is it escaped so replaceEscaped=$(sed 's/[&/\]/\\&/g' <<< "$value") # escape it # Replace the value if it exists sed -i.bak "s/^$key\=.*$/$key=\"$replaceEscaped\"/g" "$target_env_file" # Remove backup rm -f "${target_env_file}.bak" >/dev/null 2>&1 echo "Replaced value for $key in $target_env_file" else # Make sure the is a new line at the end of the config file sed -i~ '$ a\' "$CONFIG_ENV" # Append new value echo "$key=\"$value\"" | tee -a "$target_env_file" >/dev/null echo "Added value for $key into $target_env_file" fi done } config_remove () { eval $(parse_params "$@") # If --global is passed set in global docksal.env local target_env_file if [[ "$global" == "global" ]]; then target_env_file="$CONFIG_ENV" else if [[ "$env" == "" ]]; then target_env_file="$(get_project_path_dc)/.docksal/docksal.env" else target_env_file="$(get_project_path_dc)/.docksal/docksal-${env}.env" fi fi # Warn if no values were found if [[ "${#ARGV[@]}" == 0 ]]; then echo "No values to remove" return 1 fi # Loop named params array for param in "${ARGV[@]}" do if grep -q "^${param}\=" "${target_env_file}"; then # Remove the value if it exists echo "Removing ${param} from $target_env_file" sed -i.bak "/^${param}\=.*$/d" "$target_env_file" # Remove backup rm -f "${target_env_file}.bak" >/dev/null 2>&1 else echo "Could not find ${param} in ${target_env_file}" fi shift done } # Can add or remove a host to/from hosts file hosts () { is_windows || is_wsl && HOSTS_FILE="/c/windows/system32/drivers/etc/hosts" is_mac && HOSTS_FILE="/etc/hosts" is_linux && HOSTS_FILE="/etc/hosts" case "$1" in add) shift hosts_add "$1" ;; remove) shift hosts_remove "$1" ;; '') cat "$HOSTS_FILE" ;; *) show_help_hosts exit 1 ;; esac } # Add a host to hosts file hosts_add () { local hostname="$1" # Use VIRTUAL_HOST if no parameters provided if [ "$hostname" == "" ]; then check_project_environment load_configuration hostname=$(echo $VIRTUAL_HOST | sed "s/,/ /") # replace comma with space in case someone used complex VIRTUAL_HOST [ "$(echo $hostname | xargs)" == "" ] && echo-error "Hostname was not provided" && exit 1 fi [[ ! "$hostname" =~ "." ]] && echo-error "Hostname should contain '.'" && exit 1 # Always make hosts file backup hosts_backup # Add to hosts echo -e "Adding: ${yellow}$DOCKSAL_IP $hostname${NC}" if is_windows; then local tmp="/tmp/hosts.bak.$RANDOM" cp "$HOSTS_FILE" "$tmp" echo "$DOCKSAL_IP $hostname" | tee -a "$tmp" >/dev/null unix2dos -q "$tmp" winsudo "copy /Y $(cygpath -w ${tmp}) $(cygpath -w ${HOSTS_FILE}) && erase $(cygpath -w ${tmp})" else echo "$DOCKSAL_IP $hostname" | sudo tee -a "$HOSTS_FILE" >/dev/null fi } # Remove a host from hosts file hosts_remove () { local hostname="$1" if [ "$hostname" == "" ]; then check_project_environment load_configuration hostname=$(echo $VIRTUAL_HOST | sed "s/,/ /") # replace comma with space in case someone used complex VIRTUAL_HOST [ "$(echo $hostname | xargs)" == "" ] && echo-error "Hostname was not provided" && exit 1 fi [[ ! "$hostname" =~ "." ]] && echo-error "Hostname should contain '.'" && exit 1 # Double insurance from removing lines that may damage system ( [[ "$hostname" =~ "localhost" ]] || [[ "$hostname" =~ "loopback" ]] || [[ "$hostname" =~ "localnet" ]] || [[ "$hostname" =~ "allnodes" ]] ) && echo-error "Cannot remove localhost" && exit 1 [[ "$hostname" =~ "broadcasthost" ]] && echo-error "Cannot remove broadcasthost" && exit 1 # Always make hosts file backup hosts_backup # Remove from hosts echo -e "Removing lines containing ${yellow}$hostname${NC}" if is_windows; then local tmp="/tmp/hosts.bak.$RANDOM" cp "$HOSTS_FILE" "$tmp" cat "$HOSTS_FILE" | grep -v "$hostname" | sudo tee "$tmp" >/dev/null unix2dos -q "$tmp" winsudo "copy /Y $(cygpath -w ${tmp}) $(cygpath -w ${HOSTS_FILE}) && erase $(cygpath -w ${tmp})" else cat "$HOSTS_FILE" | grep -v "$hostname" | sudo tee "$HOSTS_FILE" >/dev/null fi } # Backup hosts file hosts_backup () { local bkp="/tmp/hosts.bak.$RANDOM" echo "Backing up to $bkp" cp "$HOSTS_FILE" "$bkp" } vhosts () { docker exec docksal-vhost-proxy sh -c "ls /etc/nginx/conf.d/vhosts.conf >/dev/null 2>&1" [[ "$?" != "0" ]] && echo "No virtual hosts found" && return 1 NAMES=$(docker exec docksal-vhost-proxy sh -c "grep server_name /etc/nginx/conf.d/vhosts.conf -r | sort | uniq | sed 's/;//g' | sed 's/^.*_name//g'") for vhost in $NAMES ;do echo-green "http://$vhost" done } # Addons management addon () { eval $(parse_params "$@") if [[ "$global" == "global" ]] || [[ "$g" == "g" ]]; then is_global="global" fi local addon_name=${ARGV[1]} # ARGV[0] is install, remove,... # If addon contains 3 slash separated parts treat them as path # i.e. username/repo/addon_name _regex="(.*\/.*)\/(.*)" if [[ "$addon_name" =~ $_regex ]]; then URL_ADDONS_REPO="$URL_ADDONS_HOSTING/${BASH_REMATCH[1]}" addon_name=${BASH_REMATCH[2]} fi case "$1" in install|in) addon_install "$addon_name" ${is_global} ;; remove|rm) addon_remove "$addon_name" ${is_global} ;; version|v) # special case for addon version command addon_script=$(get_addon_script "$addon_name") [ ! -f "$addon_script" ] && echo "-1" && exit addon_version=$(cat "$addon_script" | grep ^VERSION | cut -d = -f 2 | tr -d \" | tr -d \') addon_version="${addon_version:-0}" echo "$addon_version" ;; help) show_help_addon ;; *) echo-yellow "Unknown sub-command. See ${NC}fin addon help${yellow} for details." ;; esac } # Download addon # $1 - addon name # $2 - optional flag to install globally addon_install () { [[ "$2" != "global" ]] && check_project_environment && load_configuration local addon_name="$1" [[ "$addon_name" == "" ]] && echo-error "No addon name was provided" && exit 1 [[ "$addon_name" =~ / ]] && echo-error "No slashes in addon name" && exit 1 [[ "$addon_name" =~ \\ ]] && echo-error "No slashes in addon name" && exit 1 ([[ "$addon_name" == ".." ]] || [[ "$addon_name" == "." ]]) && echo-error "Invalid addon name" && exit 1 # Create target addon dir local addon_path if [[ "$2" != "global" ]]; then addon_path="$PROJECT_ROOT/$DOCKSAL_ADDONS_PATH/$addon_name" else addon_path="$HOME/$DOCKSAL_ADDONS_PATH/$addon_name" fi if [ -d "$addon_path" ] || [ -f "$addon_path" ]; then echo-yellow "Addon already exists in $addon_path" #_confirm "Overwrite?" fi mkdir -p "$addon_path" if_failed_error "Could not create $addon_path" "Check file permissions and try again" local URL_ADDON="$URL_ADDONS_REPO/master/$addon_name" # Get files list local file_list file_list=$(curl -fsL "$URL_ADDON/$addon_name.filelist") # Download only hook files first local hooks_regex="$addon_name.pre-install\|$addon_name.post-install\|$addon_name.pre-uninstall\|$addon_name.post-uninstall" if [[ "$file_list" != "" ]]; then # Skip line comments, comments are lines that start with '#' file_list=$(echo "$file_list" | grep -v '^#') # Get only hooks local hooks_file_list hooks_file_list=$(echo "$file_list" | grep "$hooks_regex") if [[ "$hooks_file_list" != "" ]]; then echo-green "Downloading addon hook files..." ( cd "$addon_path" || exit 1 for f in ${hooks_file_list}; do echo " $f" # Exit subshell with error if download failed curl -fsSL "$URL_ADDON/$f?$RANDOM" -o "$f" || exit 1 done ) if_failed_error "Download has failed" "Check log above for messages" fi fi # Pre-install hook [[ "$2" == "global" ]] && addon_global="global" addon_hook "$addon_path" "$addon_name" "pre-install" "$addon_global" if [ ! $? -eq 0 ]; then echo-red "Pre-install hook has failed and aborted the installation." # Delete downloaded files if pre-install hook fails [[ -d "$addon_path" ]] && [[ "$addon_path" =~ ".docksal" ]] && rm -r "$addon_path" return 1 fi # Download main script echo-green "Downloading addon main script" echo " $addon_name/$addon_name" curl -fsL "$URL_ADDON/$addon_name?$RANDOM" -o "$addon_path/$addon_name" chmod +x "$addon_path/$addon_name" if_failed_error "Could not get $addon_name" "Check your internet connection and try again" # Download other files if [[ "$file_list" != "" ]]; then # Exclude hooks file_list=$(echo "$file_list" | grep -v "$hooks_regex") if [[ "$file_list" != "" ]]; then echo-green "Downloading other addon files..." ( cd "$addon_path" || exit 1 for f in ${file_list}; do echo " $f" # Filename can contain subdir name, which we need to create local subdir=$(dirname "$f") if [[ "$subdir" != "." ]]; then mkdir -p "$subdir" || exit 1 fi # Exit subshell with error if download failed curl -fsSL "$URL_ADDON/$f?$RANDOM" -o "$f" || exit 1 done ) if_failed_error "Download has failed" "Check log above for messages" fi fi # Post-install hook addon_hook "$addon_path" "$addon_name" "post-install" "$addon_global" [[ $? != 0 ]] && _with_errors="${yellow}with errors${NC}" [[ "$2" == "global" ]] && _globally="globally" echo-green "Addon ${NC}$addon_name${green} was installed ${_globally} ${_with_errors}" } # Remove addon # $1 - addon name # $2 - optional flag to remove global addon addon_remove () { [[ "$2" != "global" ]] && check_project_environment && load_configuration local addon_name="$1" [[ "$addon_name" == "" ]] && echo-error "No addon name was provided" && exit 1 [[ "$addon_name" =~ / ]] && echo-error "No slashes in addon name" && exit 1 [[ "$addon_name" =~ \\ ]] && echo-error "No slashes in addon name" && exit 1 ([[ "$addon_name" == ".." ]] || [[ "$addon_name" == "." ]]) && echo-error "Invalid addon name" && exit 1 local addon_script=$(get_addon_script "$addon_name") local addon_path=$(dirname "$addon_script") # Check if found addon is a valid addon # Note: do not change this condition to -d for security and safety reasons. # Double checking for Docksal addon structure prevents possible # damage if get_addon_script returns terribly wrong path for any reason. if [[ ! -f "$addon_script" ]]; then echo-error "Could not find addon: ${NC}$1" return 1 fi # Do not remove global addon unless --global is passed if [[ "$addon_path" =~ "$CONFIG_DIR" ]] && [[ "$2" != "global" ]]; then echo-error "Refusing to remove global addon" \ "To remove global addon use ${yellow}--global${NC} or ${yellow}-g${NC} option" exit 1 fi # Pre-uninstall hook [[ "$2" == "global" ]] && addon_global="global" addon_hook "$addon_path" "$addon_name" "pre-uninstall" "$addon_global" if [ ! $? -eq 0 ]; then echo-red "Pre-uninstall hook has failed and aborted addon removal." return 1 fi # Prepare post-uninstall hook local tempfile="/tmp/$addon_name.$RANDOM.post-uninstall" local tempfile_copy_res=1 cp -fp "$addon_path/$addon_name.post-uninstall" "$tempfile" >/dev/null 2>&1 tempfile_copy_res=$? # Remove addon files physically # Last line of defence to avoid removal of wrong things if [[ "$addon_path" =~ ".docksal" ]]; then [ -d "$addon_path" ] && (echo-green "Removing $addon_path" && rm -r "$addon_path" && echo-green "Removed addon") else echo "Not removing $addon_path because it's not inside .docksal folder. How did we get here?" fi # Post-uninstall hook if [ ${tempfile_copy_res} -eq 0 ]; then ( export ADDON_GLOBAL="$addon_global"; chmod +x "$tempfile" && exec "$tempfile" ) rm -f "$tempfile" >dev/null 2>&1 fi } # Execute a hook script for an addon # $1 - addon path # $2 - addon name # $3 - hook name # $4 - "global" is addon installed as global (optional) addon_hook () { local addon_path="$1" local addon_name="$2" local hook_name="$3" local addon_global="" local hook="$addon_path/$addon_name.$hook_name" # Assign is global [[ "$4" == "global" ]] && addon_global="true" # No hook is not an error [[ ! -f "$hook" ]] && return # Execute hook echo-green "Running $hook_name hook..." && chmod +x "$hook" || return 1 # Encapsulate exec into a sub-shell because exec replaces the shell it runs in ( export ADDON_GLOBAL="$addon_global"; exec "$hook" ) } # Looks for addon/custom command by name, and returns its file path # $1 - name addon_get_path () { local addon="$(get_addon_script $1)" if [[ ! -f "$addon" ]]; then # If no addon was found then try command addon="$(get_command_script $1)" fi [[ -f "$addon" ]] && echo "$addon" } # Execute addon by its file path, passing params to it # $1 - path to file # $2..$99 - params addon_execute () { local addon="$1" # Check that file exists or fail if [[ ! -f "$addon" ]]; then echo-stderr "ERROR: addon executable $1 was not found" return 1 fi # Export ADDON_ROOT variable, that can be used in addons export ADDON_ROOT=$(dirname "$addon") # Load only global configuration first load_global_configuration # If run inside project root, then load project configuration as well [[ "$PROJECT_ROOT" != "" ]] && load_configuration # If executable is not set, then fix it if [[ ! -x "$addon" ]]; then echo -e "${yellow}$addon${NC} is not set to be executable." _confirm "Fix automatically?" chmod +x "$addon" if_failed "Could not make $addon executable" fi # If it has windows line endings, then fix it fix_crlf_warning "$addon" # Read host/cli execution settings _exec_target=$(cat "$addon" | grep '^#:[ ]*exec_target[ ]*=' | sed -E "s/^#:[ ]*exec_target[ ]*=[ ]*//g") shift if [[ "$_exec_target" == "run-cli" ]]; then cmd="$addon" [[ "$@" != "" ]] && cmd="$addon "$(printf " %q" "$@") run_cli "$cmd" elif [[ "$_exec_target" != "" ]]; then # Replace host addon path with the matching path inside cli # "\$PROJECT_ROOT" will be resolved in cli to /var/www cli_addon_path="\$PROJECT_ROOT${addon/$PROJECT_ROOT/}" cmd="$cli_addon_path" # Escape spaces that are "spaces" and not parameter delimiters (i.e. param1 param2\ with\ spaces param3) [[ "$@" != "" ]] && cmd="$cli_addon_path "$(printf " %q" "$@") CONTAINER_NAME="$_exec_target" _exec "$cmd" else exec "$addon" "$@" fi } unison_sync_wait () { # If not unison volumes exit [[ "$DOCKSAL_VOLUMES" != "unison" ]] && return echo-green "Waiting for Unison to perform initial sync..." RESPONSE_TIMER=5 # default check frequency in seconds while [[ 1 == 1 ]]; do local LOG_TAIL=$(docker-compose logs --tail=5 unison) if (echo "$LOG_TAIL" | grep -q "Synchronization complete"); then break elif (echo "$LOG_TAIL" | grep -q "Nothing to do"); then break else echo $(date +"%T") $(echo "$LOG_TAIL" | tail -1) sleep ${RESPONSE_TIMER} fi done } # Fix permissions when running as root ( e.g. on Play with Docker, Katacoda, etc.) set_project_root_permissions () { # Set permissions on project root to match the default user:group in cli. is_root && chown -R 1000:1000 "$PROJECT_ROOT" } #-------------------------- RUNTIME STARTS HERE ---------------------------- # Figure out COLUMNS and LINES (not set in scripts) have to be passed to workaround a bug in Docker. # See https://github.com/moby/moby/issues/35407#issuecomment-355753176 if is_tty; then if ( which tput >/dev/null 2>&1 ); then COLUMNS=$(tput cols) LINES=$(tput lines) # If tput is not available, try ssty. See https://stackoverflow.com/a/48016366/4550880 elif ( which stty >/dev/null 2>&1 ); then read LINES COLUMNS < <(stty size) fi export LINES COLUMNS fi # Load environment variables overrides, use to permanently override some variables # Source and allexport variables in the .env file if [[ -f "$CONFIG_ENV" ]]; then set -a source "$CONFIG_ENV" set +a else touch "$CONFIG_ENV" fi # Generate Docksal instance uuid if not available if [[ "$DOCKSAL_UUID" == "" ]]; then export DOCKSAL_UUID=$(uuid_generate) # Make sure the is a new line at the end of the config file, then write to it sed -i~ '$ a\' "$CONFIG_ENV" echo "DOCKSAL_UUID=$DOCKSAL_UUID" | tee -a "$CONFIG_ENV" >/dev/null fi # Automatically create docksal.yml and docksal.env if they do not exist # Relate to https://github.com/docksal/docksal/issues/459 if [[ "$(get_project_path)" != "" ]] ; then docksal_yml_file="$(get_project_path_dc)/.docksal/docksal.yml" docksal_env_file="$(get_project_path_dc)/.docksal/docksal.env" docksal_yml_version=$(head "$(get_config_dir_dc)/stacks/services.yml" | grep "version") # Create default docksal.yml and docksal.env files if there is no configuration yet if [[ ! -f "$docksal_yml_file" ]]; then (echo "$docksal_yml_version" > "$docksal_yml_file") 2>/dev/null if [[ ! -f "$docksal_env_file" ]]; then (echo "DOCKSAL_STACK=default" > "$docksal_env_file") 2>/dev/null else if (source "$docksal_env_file" && [[ "$DOCKSAL_STACK" == "" ]]); then (echo -e "DOCKSAL_STACK=default\n$(cat $docksal_env_file)" > "$docksal_env_file") 2>/dev/null fi fi else # Create empty docksal.env if it does not exist if [[ ! -f "$docksal_env_file" ]]; then touch "$docksal_env_file" 2>/dev/null fi fi fi # Host user uid/gid # The startup script in the cli container picks these up and updates the docker user inside of the container # On Windows this is not necessary, as Linux permissions and ownership do not matter over SMB if ! is_windows; then # Only do this for non-root users if ! is_root; then export HOST_UID=$(id -u) export HOST_GID=$(id -g) else # Note: php-fpm inside cli will fail to start as uid=0 # Don't display this in PWD and Katacoda since users do not control those environments anyway. # TODO: handle this case better if ! (is_pwd || is_katacoda); then echo-alert "Running as root is not recommended" fi fi fi # Network settings # DOCKSAL_IP - Docker/VM IP # DOCKSAL_DNS1 - IP used for DNS resolution by containers via docksal-dns (adds support for *.docksal domains) # DOCKSAL_DNS2 - a backup external DNS server if is_docker_native || is_wsl; then export DOCKSAL_DNS1=${DOCKSAL_IP} export DOCKSAL_DNS2=${DOCKSAL_DNS_UPSTREAM:-$DOCKSAL_DEFAULT_DNS} else export DOCKSAL_DNS1=${DOCKSAL_IP} export DOCKSAL_DNS2=${DOCKSAL_DNS_UPSTREAM:-$DOCKSAL_DEFAULT_DNS} fi # Create drives bind mounts on wsl wsl_mount_drives # Export environment variables to properly reach Docker server if [[ "$DOCKSAL_HOST" != "" ]]; then export DOCKER_HOST="$DOCKSAL_HOST" elif is_docker_native; then # no vm used export DOCKER_HOST="" elif is_wsl; then # no vm used export DOCKER_HOST="0.0.0.0:2375" else # get active machine and its status __current_active_machine=$(cat "$CONFIG_MACHINE_ACTIVE" 2>/dev/null || echo '') DOCKER_MACHINE_NAME="${__current_active_machine:-$DEFAULT_MACHINE_NAME}" DOCKER_MACHINE_STATUS=$(docker-machine status "$DOCKER_MACHINE_NAME" 2>&1 || echo '') if [[ "$DOCKER_MACHINE_STATUS" == 'Running' ]]; then # Use cached environment variables if possible if [[ -f "$CONFIG_MACHINES_ENV" ]]; then eval $(cat "$CONFIG_MACHINES_ENV") # if DOCKER_HOST is still not set then get environment variables once again if [[ DOCKER_HOST == "" ]]; then docker_machine_env fi fi fi # current active machine folder DOCKER_MACHINE_FOLDER="$CONFIG_MACHINES/$DOCKER_MACHINE_NAME" mkdir -p "$DOCKER_MACHINE_FOLDER" # TODO: revise or remove later. search for DOCKER_MACHINE_IP in the code # get desired ip # if [[ -f "$DOCKER_MACHINE_FOLDER/ip" ]]; then # DOCKER_MACHINE_IP=$(cat "$DOCKER_MACHINE_FOLDER/ip") #fi #DOCKER_MACHINE_IP=${DOCKER_MACHINE_IP:-$DOCKSAL_IP} fi if is_docker_running; then export DOCKER_RUNNING="true" else export DOCKER_RUNNING="false" fi # Handle Alias if [[ "$1" == "@"* ]]; then USED_ALIAS=${1#@} shift # Search alias first between aliases then between running projects if [[ "$USED_ALIAS" == "self" ]]; then _alias_cd="$(get_project_path)" elif [[ -h "$CONFIG_ALIASES/$USED_ALIAS" ]]; then # alias found _alias_cd=$(readlink "$CONFIG_ALIASES/$USED_ALIAS") else check_docker_running # alias not found. search project names _alias_projects=$(docker ps --all \ --filter 'label=io.docksal.project-root' \ --format '{{.Label "com.docker.compose.project"}}:{{.Label "io.docksal.project-root"}}') if (echo "$_alias_projects" | grep "^$USED_ALIAS:" >/dev/null 2>&1); then # project found _alias_cd=$(echo "$_alias_projects" | grep "^$USED_ALIAS:" | cut -d$':' -f 2) else # nothing was found. error out echo-error "No such fin alias: @$USED_ALIAS" exit 1 fi fi [[ "$1" == "" ]] && echo "$_alias_cd" && exit cd "$_alias_cd" 2>/dev/null if_failed_error "Could not navigate to directory linked by $1 alias" fi # Parse command line parameters case "$1" in bash_comp_words) # no load_configuration shift bash_comp_words "$@" ;; build) load_configuration shift docker-compose build "$@" ;; init) # no load configuration addon_file_path="$(addon_get_path $1)" shift # If init exists as an addon/command, then execute it instead if [[ ! -z "$addon_file_path" ]]; then addon_execute "$addon_file_path" "$@" else init "$@" fi ;; start) load_configuration check_for_updates shift up ;; up) # Force re-reading of configuration files COMPOSE_FILE="" ENV_FILE="" DOCKSAL_VOLUMES="" load_configuration check_for_updates shift up ;; stop) shift # load_configuration is called later in _stop_containers stop "$@" ;; restart) load_configuration shift restart "$@" ;; reset) shift if [[ "dns proxy ssh-agent network system" =~ "$1" ]]; then echo-warning "${NC}Deprecated. Use ${yellow}fin system reset${NC} to reset Docksal system services" exit 1 fi load_configuration reset "$@" ;; remove|rm) load_configuration shift remove "$@" ;; image) # no load_configuration shift case "$1" in save) shift [[ "$1" == "--project" ]] && load_configuration image_save "$@" ;; load) shift image_load "$@" ;; registry) shift image_registry_list "$@" ;; *) show_help_image exit 1 ;; esac ;; project|p) # no load_configuration shift case "$1" in create) shift project_create "$@" ;; list|ls) shift project_list "$@" ;; '') echo -e "start stop list status ... (${yellow}fin help project${NC})" exit 1 ;; status|st) load_configuration check_for_updates project_status ;; start) load_configuration check_for_updates shift up ;; up) # Force re-reading of configuration files COMPOSE_FILE="" ENV_FILE="" DOCKSAL_VOLUMES="" load_configuration check_for_updates shift up ;; stop) shift # load_configuration is called later in _stop_containers stop "$@" ;; restart) load_configuration shift restart "$@" ;; reset) shift check_docker_running load_configuration reset "$@" ;; remove|rm) load_configuration shift remove "$@" ;; erase) load_configuration shift project_erase "$@" ;; config) load_configuration check_for_updates config_show "$@" ;; build) load_configuration shift docker-compose build "$@" ;; help) show_help_project ;; *) show_help_project exit 1 ;; esac ;; system|sys) # no load_configuration shift case "$1" in reset) shift # Make sure docker daemon is accessible. reset network is a special excluded case. [[ ! "network" =~ "$1" ]] && check_docker_running system_reset "$@" ;; start) shift system_start ;; status|st) shift system_status ;; stop) shift system_stop ;; *) show_help_system exit 1 ;; esac ;; status|ps) load_configuration check_for_updates project_status ;; # TODO: remove "projects" alias in July 2017 projects|pl) # no load_configuration check_for_updates shift project_list "$@" ;; vm) # no load_configuration shift vm "$@" ;; update) if [[ "$DOCKSAL_LOCK_UPDATES" != "" ]]; then echo-error "Updates locked in this environment" exit 1 fi # no load_configuration shift if [[ "$1" == "--system-images" ]]; then update_system_images elif [[ "$1" == "--project-images" ]]; then update_project_images elif [[ "$1" == "--self" ]]; then fin_url=$(get_fin_url) echo "Downloading ${fin_url}" curl -kfsSL "${fin_url}?r=$RANDOM" | sudo tee "$FIN_PATH_UPDATED" >/dev/null [[ "$2" == "--diff" ]] && diff "$FIN_PATH" "$FIN_PATH_UPDATED" sudo cp "$FIN_PATH_UPDATED" "$FIN_PATH" [ $? -eq 0 ] && echo "Done" exit elif [[ "$1" == "--tools" ]]; then update_tools # TODO: "--config" deprecated, remove at some point elif [[ "$1" == "--config" ]] || [[ "$1" == "--stack" ]] || [[ "$1" == "--stacks" ]]; then echo-green "Updating stack files..." update_config_files elif [[ "$1" == "--bash-complete" ]]; then install_bash_autocomplete elif [[ "$1" != "" ]]; then echo -e "${yellow}fin update${NC} does not support this parameter" else update fi ;; bash) load_configuration shift _bash "$@" ;; exec|run) load_configuration shift if [ -f "$1" ]; then # if a file is passed then run it inside cli container [ "$(get_project_path)" == "" ] && echo "Should be run inside a project" && exit 1 # If file does not include path append ./ if [[ "$(basename $1)" == "$1" ]]; then script_to_run="./$1" else script_to_run="$1" fi _exec "PROJECT_ROOT=/var/www DOCROOT=$DOCROOT VIRTUAL_HOST=$VIRTUAL_HOST /bin/bash -c $script_to_run" else _exec "$@" fi ;; run-cli|rc) # no load_configuration shift run_cli "$@" ;; # TODO: Remove mysql and sql* commands in fin 2.0 mysql|sqlc) load_configuration shift _mysql "$@" ;; mysql-list|sqls) load_configuration shift mysql_list "$@" ;; mysql-import|sqli) load_configuration shift mysql_import "$@" ;; mysql-dump|sqld) load_configuration shift mysql_dump "$@" ;; db) load_configuration shift case "$1" in cli) shift _mysql "$@" ;; list|ls) shift mysql_list "$@" ;; import) shift mysql_import "$@" ;; dump) shift mysql_dump "$@" ;; create) shift mysql_db_create "$@" ;; drop) shift mysql_db_drop "$@" ;; truncate) shift mysql_db_truncate "$@" ;; *) show_help_db exit 1 ;; esac ;; drush) load_configuration shift # cd to docroot if alias was used and alias leads to project root [[ "$USED_ALIAS" != "" ]] && [[ "$(pwd)" == "$PROJECT_ROOT" ]] && cd "$DOCROOT" _exec drush "$@" ;; drupal) load_configuration shift _exec drupal "$@" ;; terminus) load_configuration shift _exec terminus "$@" ;; platform) load_configuration shift _exec platform "$@" ;; wp) load_configuration shift if [[ "$1" == "" ]]; then _exec wp else _exec wp "$@" fi ;; composer) load_configuration shift _exec composer "$@" ;; ssh-add) # no load_configuration shift check_docker_running ssh_add "$@" ;; docker|d) # no load_configuration shift is_docker_running # exports env check_winpty_found ${winpty} docker "$@" ;; docker-compose|dc) load_configuration shift is_docker_running # exports env docker-compose "$@" ;; docker-machine|dm) # no load_configuration shift is_docker_running # exports env docker-machine "$@" ;; debug) # no load_configuration shift # Support debug with project configuration loading if [[ "$1" == "-c" ]] || [[ "$1" == "--load-configuration" ]]; then shift load_configuration fi eval "$@" ;; exec-url) # no load_configuration shift exec_url "$@" ;; share) load_configuration shift ngrok_share "$@" ;; cleanup) # no load_configuration check_for_updates shift cleanup $1 ;; -v | v) # no load_configuration check_for_updates version --short ;; --version | version) # no load_configuration check_for_updates version ;; logs) load_configuration shift logs "$@" ;; "") # no load_configuration check_for_updates show_help ;; help) # no load_configuration check_for_updates show_help "$2" ;; sysinfo) # no load_configuration check_for_updates shift sysinfo "$@" ;; diagnose) # no load_configuration shift diagnose "$@" ;; config) # different load_configuration shift case "$1" in generate) # no load_configuration shift config_generate "$@" ;; set) # load_configuration is conditional in function shift config_set "$@" ;; get) # load_configuration is conditional in function shift config_get "$@" ;; remove|rm) # load_configuration is conditional in function shift config_remove "$@" ;; *) load_configuration check_for_updates config_show "$@" ;; esac ;; fix-smb) # no load_configuration smb_windows_fix ;; alias) # no load_configuration shift [[ "$*" == "" ]] && alias_list && exit [[ "$*" == "list" ]] && alias_list && exit [[ "$1" == "remove" ]] && shift && alias_remove "$@" && exit alias_create "$@" ;; hosts) # no load_configuration shift hosts "$@" ;; vhosts) # no load_configuration vhosts ;; addon|a) # configuration will be loaded by end functions if needed shift addon "$@" ;; *) addon_file_path="$(addon_get_path $1)" if [[ -z "$addon_file_path" ]]; then echo-yellow "Unknown command ${NC}$*${yellow}. See 'fin help' for the list of available commands" exit 1 fi shift addon_execute "$addon_file_path" "$@" ;; esac