#!/usr/bin/env bash # To override Docksal version during: # - install - DOCKSAL_UPDATE_VERSION=develop bash <(curl -fsSL https://get.docksal.io) # - update - DOCKSAL_UPDATE_VERSION=develop fin update # Support the historical "DOCKSAL_VERSION" version override parameter DOCKSAL_UPDATE_VERSION="${DOCKSAL_UPDATE_VERSION:-$DOCKSAL_VERSION}" # Set current version constants FIN_VERSION=1.118.0 DOCKSAL_VERSION=v1.19.0 # Predefined terminal colors # Format: '\033[;;m' # See https://misc.flogisoft.com/bash/tip_colors_and_formatting # Note: Using "\033" (and not "\e") as it seems to have a wider support in different shells. if [[ "$TERM" != "dumb" ]]; then # Console colors green='\033[0;32;49m' green_bg='\033[0;42;30m' yellow='\033[0;33;49m' yellow_bold='\033[1;33;49m' yellow_bg='\033[0;43;30m' red='\033[0;91;49m' red_bg='\033[0;101;30m' blue='\033[0;34;49m' lime='\033[0;92;49m' acqua='\033[0;96;49m' magenta='\033[0;35;49m' lightmagenta='\033[0;95;49m' lightmagenta_bg='\033[0;105;30m' 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" ]] || # ID_LIKE is a list of operating system identifiers (e.g., Pop!_OS has ID_LIKE="debian ubuntu") [[ "$OS_ID_LIKE" =~ "ubuntu" ]] || [[ "$OS_ID_LIKE" =~ "debian" ]] } is_alpine () { [[ "$OS_ID" == "alpine" ]] } is_fedora () { [[ "$OS_ID" == "fedora" ]] || [[ "$OS_ID" == "centos" ]] } # TODO: drop this function in May 2020 to complete Babun deprecation is_windows () { [[ "$OS_TYPE" == "Cygwin" ]] } is_wsl () { [[ "$OS_TYPE" == "WSL" ]] } is_mac () { [[ "$OS_TYPE" == "Darwin" ]] } # macOs VirtualBox is_mac_vbox () { is_mac && ! is_docker_native } # macOs Docker Desktop HyperKit is_mac_dd_hk () { is_mac && is_docker_native && ! is_mac_dd_vf } # macOs Docker Desktop Virtualization Framework # TODO: figure out a better/more reliable check is_mac_dd_vf () { is_mac && is_docker_native && (ifconfig | grep 192.168.81.1 > /dev/null) } # 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_github_actions () { [[ "$GITHUB_RUN_ID" != "" ]] } is_gitpod () { [[ "$GITPOD_HOST" != "" ]] } # Returns x86_64 / aarch64 (aka "old school") host_arch () { case $(uname -m) in x86_64|amd64) echo "x86_64" ;; aarch64|arm64) echo "aarch64" ;; * ) uname -m;; # Catch-all for anything else esac } # Returns amd64 / arm64 (aka "new style") host_arch_new () { case $(uname -m) in x86_64|amd64) echo "amd64" ;; aarch64|arm64) echo "arm64" ;; * ) uname -m;; # Catch-all for anything else esac } is_supported_arch () { [[ "$(host_arch_new)" =~ ^amd64|arm64$ ]] } #------------------------------------------------------------------------------ # 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/')" } # Returns the value of Windows environment var in WSL # @param $1 var name wsl_env () { # Trim \r from output (necessary on Windows) # "cd /mnt/c" is a workaround to fix a warning message in Windows 10 v1903+ # See https://github.com/docksal/docksal/issues/1103#issuecomment-522160146 echo $(cd /mnt/c && cmd.exe /c "echo %$1%" | tr -d '\r') } # Unfortunately wslpath output contains \r that has to be cleaned wsl_path () { # Trim \r from output (necessary on Windows) echo $(wslpath "$@" | tr -d '\r') } # Convert version string like 1.2.3.4 to integer for comparison # param $1 version string of 4 components max (e.g., 1.10.3.0) ver_to_int () { echo "$@" | awk -F. '{ printf("%d%03d%03d%03d", $1,$2,$3,$4); }' } #---------------------------- Global Constants -------------------------------- # Dependency versions REQUIREMENTS_DOCKER='26.0.0' REQUIREMENTS_DOCKER_DD='26.0.0' # Ships with Docker Desktop 4.29.0 (145265) REQUIREMENTS_DOCKER_B2D='19.03.5' # This is the final boot2docker version REQUIREMENTS_DOCKER_COMPOSE='2.26.1' # Docker Compose V2 REQUIREMENTS_DOCKER_MACHINE='0.16.2' # This is the final docker-machine version REQUIREMENTS_VBOX='6.1.42' # Remember to update the download URLs below REQUIREMENTS_WINPTY='0.4.3' REQUIREMENTS_WINPTY_CYGWIN='2.8.0' REQUIREMENTS_DOCKER_DESKTOP='4.29.0' # Build 145265 # VirtualBox download URLs # Remember to update REQUIREMENTS_VBOX above URL_VBOX_MAC="https://download.virtualbox.org/virtualbox/6.1.42/VirtualBox-6.1.42-155177-OSX.dmg" URL_VBOX_WIN="https://download.virtualbox.org/virtualbox/6.1.42/VirtualBox-6.1.42-155177-Win.exe" # Configuration paths # Rewrite $HOME on windows to be absolute path in Unix notation if is_windows; then HOME=$(cygpath_abs_unix ${HOME}) fi # Windows user home directory # WIN_HOME is a Unix path, e.g. /c/Users/user if is_windows; then WIN_HOME=$(cygpath -u ${USERPROFILE}) elif is_wsl; then WIN_HOME=$(wsl_path -u $(wsl_env "USERPROFILE")) fi # Directory structure in ~/.docksal CONFIG_DIR="$HOME/.docksal" CONFIG_DOWNLOADS_DIR="$CONFIG_DIR/downloads" CONFIG_BIN_DIR="$CONFIG_DIR/bin" CONFIG_STACKS_DIR="$CONFIG_DIR/stacks" CONFIG_DOCKER_MACHINE_DIR="$CONFIG_DIR/machine" CONFIG_ALIASES="$CONFIG_DIR/alias" mkdir -p "$CONFIG_DOWNLOADS_DIR" mkdir -p "$CONFIG_BIN_DIR" mkdir -p "$CONFIG_STACKS_DIR" mkdir -p "$CONFIG_DOCKER_MACHINE_DIR" mkdir -p "$CONFIG_ALIASES" # Custom certs folder for vhost-proxy (overridable, expected to be created by the user manually) CONFIG_CERTS=${CONFIG_CERTS:-$CONFIG_DIR/certs} # File/binary paths FIN_PATH="/usr/local/bin/fin" FIN_PATH_UPDATED="$CONFIG_DOWNLOADS_DIR/fin.updated" FIN_AUTOCOMPLETE_PATH="/usr/local/bin/fin-bash-autocomplete" CONFIG_ENV="$CONFIG_DIR/docksal.env" CONFIG_LAST_CHECK="$CONFIG_DIR/.last_check" CONFIG_LAST_PING="$CONFIG_DIR/.last_ping" CONFIG_LAST_UPDATE="$CONFIG_DIR/.last_update" CONFIG_SSHPROXY_PID="$CONFIG_DIR/.sshproxy.pid" DOCKER_BIN="$CONFIG_BIN_DIR/docker" DOCKER_COMPOSE_BIN="$CONFIG_BIN_DIR/docker-compose" DOCKER_MACHINE_BIN="$CONFIG_BIN_DIR/docker-machine" WINPTY_BIN="$CONFIG_BIN_DIR/winpty" vboxmanage="VBoxManage" is_windows && vboxmanage="/cygdrive/c/Program Files/Oracle/VirtualBox/VBoxManage.exe" is_wsl && vboxmanage="/mnt/c/Program Files/Oracle/VirtualBox/VBoxManage.exe" CONFIG_DOCKER_MACHINE_ENV="$CONFIG_DOCKER_MACHINE_DIR/machine.env" # Custom certs folder for vhost-proxy (overridable) CONFIG_CERTS=${CONFIG_CERTS:-$CONFIG_DIR/certs} # Host path to make accessible to the VM (Mac only) DOCKSAL_NFS_PATH="${DOCKSAL_NFS_PATH:-/Users}" # Where custom commands live (relative path) DOCKSAL_COMMANDS_PATH=".docksal/commands" # Where addons live (relative path) DOCKSAL_ADDONS_PATH=".docksal/addons" # Docksal environment # Exported to be visible in custom commands and addons by default. export 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" # Allow turning built-in DNS features on/off. Set to "1" to switch to external DNS (which will be standard in v2) DOCKSAL_DNS_DISABLED="${DOCKSAL_DNS_DISABLED:-1}" # 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_IP="${DOCKSAL_DNS_IP}" DOCKSAL_DNS_UPSTREAM="${DOCKSAL_DNS_UPSTREAM}" DOCKSAL_DNS_DOMAIN_DEFAULT="docksal" DOCKSAL_DNS_DOMAIN="${DOCKSAL_DNS_DOMAIN:-$DOCKSAL_DNS_DOMAIN_DEFAULT}" # Allow disabling the DNS resolver configuration (in case there are issues with it). Set to "1" to activate. DOCKSAL_NO_DNS_RESOLVER="${DOCKSAL_NO_DNS_RESOLVER:-0}" # 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}" # Docker for Windows DOCKER_WIN_NETWORK="Loopback Pseudo-Interface 1" # 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}" DOCKSAL_VHOST_PROXY_DEFAULT_CERT="${DOCKSAL_VHOST_PROXY_DEFAULT_CERT}" PROJECT_INACTIVITY_TIMEOUT="${PROJECT_INACTIVITY_TIMEOUT}" PROJECT_DANGLING_TIMEOUT="${PROJECT_DANGLING_TIMEOUT}" PROJECT_AUTOSTART="${PROJECT_AUTOSTART}" PROJECTS_ROOT="${PROJECTS_ROOT}" DEFAULT_MACHINE_NAME='docksal' DEFAULT_MACHINE_PROVIDER='virtualbox' DEFAULT_MACHINE_VBOX_RAM="${DEFAULT_MACHINE_VBOX_RAM:-2048}" #mb DEFAULT_MACHINE_VBOX_HDD="${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} # Whether to prevent (checking for) updates for Docksal DOCKSAL_LOCK_UPDATES="${DOCKSAL_LOCK_UPDATES:-0}" # The TCP listen port on DOCKSAL_HOST_IP for the SSH proxy, if used DOCKSAL_SSH_PROXY_PORT="${DOCKSAL_SSH_PROXY_PORT:-30001}" # 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}" # SSH agent settings if is_ci; then # Prefer the host's agent in CI environments DOCKSAL_SSH_AGENT_USE_HOST="${DOCKSAL_SSH_AGENT_USE_HOST:-1}" else # Use the built-in agent in non-CI environments DOCKSAL_SSH_AGENT_USE_HOST="${DOCKSAL_SSH_AGENT_USE_HOST:-0}" fi # Project healthcheck timeout # Total time (seconds) before the _project_ healthcheck is considered failed DOCKSAL_HEALTHCHECK_TIMEOUT=${DOCKSAL_HEALTHCHECK_TIMEOUT:-60} # Container Logging settings DOCKSAL_CONTAINER_LOG_MAX_SIZE=${DOCKSAL_CONTAINER_LOG_MAX_SIZE:-1m} DOCKSAL_CONTAINER_LOG_MAX_FILE=${DOCKSAL_CONTAINER_LOG_MAX_FILE:-10} # Container healthcheck settings # Default health-interval option for all containers # Note: Setting this too low (<5s) will result in a considerable load with lots of running containers (20+) DOCKSAL_CONTAINER_HEALTHCHECK_INTERVAL=${DOCKSAL_CONTAINER_HEALTHCHECK_INTERVAL:-10s} #---------------------------- 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_RELEASE_NOTES="https://docksal.io/updates" URL_GITHUB_SPONSOR="https://github.com/sponsors/docksal" URL_REPO_DRUPAL7="https://github.com/docksal/boilerplate-drupal7.git" URL_REPO_DRUPAL10COMPOSER="https://github.com/docksal/boilerplate-drupal10-composer.git" URL_REPO_DRUPAL10BLT="https://github.com/docksal/boilerplate-drupal10-blt.git" URL_REPO_WORDPRESS="https://github.com/docksal/boilerplate-wordpress.git" URL_REPO_MAGENTO="https://github.com/docksal/boilerplate-magento.git" URL_REPO_GRAV="https://github.com/docksal/boilerplate-grav.git" URL_REPO_GATSBY="https://github.com/docksal/boilerplate-gatsby.git" URL_REPO_ANGULAR="https://github.com/docksal/boilerplate-angular.git" URL_REPO_LARAVEL="https://github.com/docksal/boilerplate-laravel.git" URL_REPO_HUGO="https://github.com/docksal/boilerplate-hugo.git" URL_REPO_SYMFONY_SKELETON="https://github.com/docksal/boilerplate-symfony-skeleton.git" URL_REPO_SYMFONY_WEBAPP="https://github.com/docksal/boilerplate-symfony-webapp.git" URL_REPO_BACKDROP="https://github.com/docksal/boilerplate-backdrop.git" URL_ADDONS_HOSTING="https://raw.githubusercontent.com" URL_ADDONS_REPO="$URL_ADDONS_HOSTING/docksal/addons" STACKS_OVERRIDES_CLOUDFLARED="overrides-cloudflared.yml" STACKS_OVERRIDES_GITPOD="overrides-gitpod.yml" STACKS_OVERRIDES_DD_BIND="overrides-dd-bind.yml" STACKS_OVERRIDES_IDE="overrides-ide.yml" STACKS_OVERRIDES_XHPROF="overrides-xhprof.yml" STACKS_SERVICES="services.yml" STACKS_STACK_ACQUIA="stack-acquia.yml" STACKS_STACK_DEFAULT="stack-default.yml" STACKS_STACK_DEFAULT_NODB="stack-default-nodb.yml" STACKS_STACK_NODE="stack-node.yml" STACKS_STACK_PANTHEON="stack-pantheon.yml" STACKS_STACK_PLATFORMSH="stack-platformsh.yml" STACKS_VOLUMES_BIND="volumes-bind.yml" STACKS_VOLUMES_NFS="volumes-nfs.yml" STACKS_VOLUMES_NONE="volumes-none.yml" STACKS_VOLUMES_UNISON="volumes-unison.yml" URL_DOCKER_COM="https://get.docker.com/" URL_DOCKER_MAC="https://download.docker.com/mac/static/stable/$(host_arch)/docker-${REQUIREMENTS_DOCKER}.tgz" URL_DOCKER_NIX="https://download.docker.com/linux/static/stable/$(host_arch)/docker-${REQUIREMENTS_DOCKER}.tgz" # Docker stopped publishing docker client binaries for Windows. They are now available from Microsoft. # See https://github.com/docker/for-win/issues/1460#issuecomment-442585850 # MS uses a different naming convention, e.g. "docker-18-09-2.zip" instead of "docker-18.09.2.zip". URL_DOCKER_WIN="https://dockermsft.blob.core.windows.net/dockercontainer/docker-${REQUIREMENTS_DOCKER//./-}.zip" # docker-compose v2 URL_DOCKER_COMPOSE_MAC="https://github.com/docker/compose/releases/download/v${REQUIREMENTS_DOCKER_COMPOSE}/docker-compose-darwin-$(host_arch)" URL_DOCKER_COMPOSE_NIX="https://github.com/docker/compose/releases/download/v${REQUIREMENTS_DOCKER_COMPOSE}/docker-compose-linux-$(host_arch)" URL_DOCKER_COMPOSE_WIN="https://github.com/docker/compose/releases/download/v${REQUIREMENTS_DOCKER_COMPOSE}/docker-compose-windows-$(host_arch).exe" # docker-machine (x86_64 only on Mac/Windows) 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-$(host_arch)" URL_DOCKER_MACHINE_WIN="https://github.com/docker/machine/releases/download/v${REQUIREMENTS_DOCKER_MACHINE}/docker-machine-Windows-x86_64.exe" # boot2docker URL_BOOT2DOCKER="https://github.com/boot2docker/boot2docker/releases/download/v${REQUIREMENTS_DOCKER_B2D}/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.4} IMAGE_VHOST_PROXY=${IMAGE_VHOST_PROXY:-docksal/vhost-proxy:1.8} IMAGE_DNS=${IMAGE_DNS:-docksal/dns:1.2} IMAGE_RUN_CLI=${IMAGE_RUN_CLI:-docksal/cli:php8.3-3.8} #---------------------------- 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-notice () { echo -e "${lightmagenta_bg} NOTICE: ${NC} ${lightmagenta}$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. # "command " calls the binary directly ignoring any overriding shell functions if is_windows; then command pwd -L | sed 's/^\/cygdrive//' elif is_wsl; then command pwd -L | sed 's/^\/mnt//' else command pwd -L fi } uuid_generate () { od -x -N16 /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 # By default, we use the built-in docksal-ssh-agent unless DOCKSAL_SSH_AGENT_USE_HOST=1 # To use host's ssh-agent on Linux, we can directly mount the SSH auth socket into the container. # To use host's ssh-agent on on Mac/Win, we proxy the Unix socket through TCP on DOCKSAL_IP. if ! is_linux || [[ "$DOCKSAL_SSH_AGENT_USE_HOST" != "1" ]]; then unset SSH_AUTH_SOCK fi # Create a "shadow" ssh-agent socket for cli on a Linux server (used for ssh-agent forwarding) # This "black magic" is necessary to avoid issues with ssh-agent forwarding on Linux sandbox servers. # Without this, cli container would start with a forwarded ssh-agent socket mounted from an admin or docksal/ci-agent # session. When the session is disconnected, the mounted socket would be removed, which makes the mount invalid and # thus results in a broken stack when vhost-proxy attempts to start a stopped project. # docksal/vhost-proxy operates with docker commands and cannot update config for containers in the stack automatically. if is_ci && [[ "${SSH_AUTH_SOCK}" != "" ]]; then # Derive a custom ssh-agent socket path from the original value # The socket must reside on the same volume or we'll get "Invalid cross-device link" # Example: SSH_AUTH_SOCK=/tmp/ssh-KweeHCNAIt/agent.1909 => SSH_AUTH_SOCK_DIR /tmp/.docksal/project export SSH_AUTH_SOCK_DIR="/$(echo ${SSH_AUTH_SOCK} | cut -d'/' -f2)/.docksal/${COMPOSE_PROJECT_NAME_SAFE}" # Create socket directory under the current user, otherwise docker will create as root, which will case issues mkdir -p "$SSH_AUTH_SOCK_DIR" # Fix permissions in cases when the volume directory has been already created by docker (root user) [[ ! -O "$SSH_AUTH_SOCK_DIR" ]] && sudo chown $(id -u):$(id -g) "$SSH_AUTH_SOCK_DIR" # find project ssh sockets and delete unused for socket in $(find ${SSH_AUTH_SOCK_DIR} -type s); do # Remove unused socket (SSH_AUTH_SOCK=${socket} && ssh-add -L >/dev/null 2>&1) || rm -f ${socket} done AGENT_ID=$(basename ${SSH_AUTH_SOCK}) # Create socket link unless one already exits if [[ ! -S "$SSH_AUTH_SOCK_DIR/$AGENT_ID" ]] then ln -f ${SSH_AUTH_SOCK} "$SSH_AUTH_SOCK_DIR/$AGENT_ID" fi # Override ssh-agent socket with out shadow copy export SSH_AUTH_SOCK="$SSH_AUTH_SOCK_DIR/$AGENT_ID" else # When there's no SSH_AUTH_SOCK, we have to provide a dummy SSH_AUTH_SOCK_DIR, # otherwise cli will have an invalid volume definition (see stacks/services.yml:cli) export SSH_AUTH_SOCK_DIR="/tmp/.docksal/${COMPOSE_PROJECT_NAME_SAFE}" mkdir -p "$SSH_AUTH_SOCK_DIR" # Fix permissions in cases when the volume directory has been already created by docker (root user) [[ ! -O "$SSH_AUTH_SOCK_DIR" ]] && sudo chown $(id -u):$(id -g) "$SSH_AUTH_SOCK_DIR" fi # Call docker compose (v2) # As a plugin for docker cli, it has to be called as "/path/to/binary compose" # This would not be necessary if the binary was installed in the default location # (~/.docker/cli-plugins/docker-compose). However, we install it in ~/.docksal/bin/docker-compose # to prevent conflicts with system binaries. # "command " calls the binary directly ignoring any overriding shell functions command docker-compose "$@" } # 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 [[ "$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 for a default command in a group [ ! -f "$command_script" ] && command_script="$COMMANDS_ROOT/$1/$1" # Then search global docksal commands directory for the command [ ! -f "$command_script" ] && command_script="$HOME/$DOCKSAL_COMMANDS_PATH/$1" # Then search for a default command in a global command group [ ! -f "$command_script" ] && command_script="$HOME/$DOCKSAL_COMMANDS_PATH/$1/$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 project container id by service name # @param $1 docker compose service name (e.g., cli) # @return docker container id get_project_container_id () { local project=$COMPOSE_PROJECT_NAME_SAFE local service=$1 # Trim \r from output (necessary on Windows) echo $(docker ps -q --no-trunc \ --filter="label=com.docker.compose.project=${project}" \ --filter="label=com.docker.compose.service=${service}" \ --filter="status=running" 2>/dev/null | tr -d '\r') } # Run command on Windows with elevated privileges winsudo () { if is_wsl; then # TODO: WIP # Starting an elevated cmd.exe and passing command(s) to it. # Make sure to enclose them in quotes like this: # winsudo "''" powershell.exe Start-Process -Wait -Verb RunAs cmd.exe -Args "'/C', '$@'" else # TODO: delete in May 2020, backward compatibility wth Babun/Cygwin cygstart --action=runas cmd.exe /C "$@" fi } # Run command on Windows and WSL with elevated privileges sudowin () { local command="$1" local arguments="" shift while [[ "$1" != "" ]]; do if [[ "$arguments" == "" ]]; then arguments="'\"$1\"'" else arguments="$arguments,'\"$1\"'" fi shift done if [[ "$arguments" != "" ]]; then powershell.exe Start-Process -Wait -Verb runAs -FilePath "$command" -Args "$arguments" else powershell.exe Start-Process -Wait -Verb runAs -FilePath "$command" fi } # 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) # Puts un-named params as-is into ${ARGV[*]} array # Additionally puts all named params as-is into ${ARGN[*]} array # Additionally puts all standalone "option" params as-is into ${ARGO[*]} array # @author Oleksii Chekulaiev # @version v1.4.1 (Jul-27-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, and quotes to prevent string breakage _escaped=${1/\*/\'\"*\"\'} _escaped=${_escaped//\'/\\\'} _escaped=${_escaped//\"/\\\"} # If equals delimited named parameter nonspace="[^[:space:]]" if [[ "$1" =~ ^${nonspace}${nonspace}*=..* ]]; then # Add to named parameters array echo "ARGN+=('$_escaped');" # key is part before first = local _key=$(echo "$1" | cut -d = -f 1) # Just add as non-named when key is empty or contains space if [[ "$_key" == "" || "$_key" =~ " " ]]; then echo "ARGV+=('$_escaped');" shift continue fi # 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 # 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" =~ ^\-${nonspace}+ ]]; then # remove dashes local _key=${1//\-} # Just add as non-named when key is empty or contains space if [[ "$_key" == "" || "$_key" =~ " " ]]; then echo "ARGV+=('$_escaped');" 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" ]] } is_boot2docker () { ! is_linux && ! is_docker_native } # 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 if 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' } # Checks if docker is accessible # DOCKER_HOST, set at the beginning of runtime, defines how we talk to the docker daemon # Returns 0 - we can successfully reach the docker daemon # Returns 1 - docker daemon is inaccessible # Returns 255 - docker-machine is down is_docker_running () { # Running "docker version" is sufficient to check if docker is running in any host environment setup. # This operation is instant even if docker is not running (assuming a socket is used). # However, if DOCKER_HOST is an HTTP/TLS endpoint and it is down, then this check will take over a minute to fail. # To mitigate this case, we first check for is_docker_machine_running when in docker-machine mode. if is_boot2docker; then # Return a distinct code if the machine is down. # This is checked down the road. is_docker_machine_running || return 255 fi docker version &>/dev/null || return 1 } is_docksal_running () { # Verify DOCKSAL_IP is assigned to the appropriate network interface. # Applicable to Docker Desktop (Mac/Windows) and Linux only. # This check is redundant, since if the IP is not assigned, Docksal system services won't start and the check below # will fail. However, there could be cases, when system services have been already started and after that hosts # network settings are adjusted/corrupted externally. is_mac && is_docker_native && ! check_network_mac && return 1 is_linux && ! is_gitpod && ! check_network_alpine && return 1 is_windows && is_docker_native && ! check_network_windows && return 1 is_wsl && is_docker_native && ! check_network_wsl && return 1 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 # Skip if built-in DNS is disabled if [[ "$DOCKSAL_DNS_DISABLED" == "0" ]]; then (echo "$system_services" | grep 'docksal-dns' >/dev/null 2>&1) || return 1 fi return 0 } # Check whether we have a working tty. # We assume the environment is interactive if there is a tty. # See http://stackoverflow.com/questions/911168/how-to-detect-if-my-shell-script-is-running-through-a-pipe/911213#911213 is_tty () { [[ -t 0 ]] } # Checks whether fin is run under root privileges is_root () { [[ "$(id -u)" == "0" ]] } #---------------------------- Other helper functions ------------------------------- testing_warn () { [[ "${DOCKSAL_UPDATE_VERSION}" != "" ]] && is_tty && \ echo-yellow "[!] Using Docksal version: ${DOCKSAL_UPDATE_VERSION}" } #---------------------------- 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 && ! is_gitpod; then configure_network_alpine configure_resolver_alpine fi check_project_root && check_docksal_running } check_docker_server_version () { # Prevent multiple alerts [[ ${DOCKER_VERSION_ALERT_SUPPRESS} == 1 ]] && return if ! is_docker_server_version; then # Since we do not control Docker Desktop, ask user to update manually when necessary if is_docker_native; then echo-notice "Please update Docker Desktop manually." \ "Expected Docker Desktop version: ${REQUIREMENTS_DOCKER_DESKTOP}" else echo-notice "Required Docker server version is $REQUIREMENTS_DOCKER" \ "Run ${yellow}fin update${NC} to update to the required version." \ "" fi fi # Prevent multiple alerts DOCKER_VERSION_ALERT_SUPPRESS=1 } # @param --quiet - don't ask for confirmation to start the machine check_docker_running () { eval $(parse_params "$@") # Check cached value (saves ~200ms) if [[ "$DOCKER_RUNNING" == "true" ]]; then # If docker is running, perform docker server / docker desktop version checks # --quiet is used during update to suppress this check until the end of the update process [[ "$quiet" == "" ]] && check_docker_server_version # Since Docker is running and we can talk to it, no more checks are necessary. Return here. return fi # 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} == 0 ]] && return # Status code 255 is returned only for docker-machine mode, when the machine exists, but it is not running. if [[ ${docker_status} == 255 ]]; then if [[ "$quiet" == "" ]]; then echo-yellow "It looks like '$DEFAULT_MACHINE_NAME' docker machine is not running." _confirm "Start it now?" fi # docker_machine_start will quit here if something fails. docker_machine_start return $? fi if is_linux; then # Remind the user about running 'newgrp docker' after install. if ! (id -nG | grep docker &>/dev/null) && ! 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 # Docker machine mode # Check for certificates error if ! is_docker_native && (docker-machine env --shell=bash "$DEFAULT_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 # Docker Desktop may not be running if is_docker_native; then if is_wsl; then echo-error "Docker Desktop is not running or cannot be accessed" \ "WHAT TO DO?" \ "1. Start Docker Desktop, wait for Docker to start." \ "2. Make sure 'Expose daemon on tcp://localhost:2375 without TLS' in Docker Desktop settings is set." exit 1 else echo-error "Docker Desktop is not running" \ "WHAT TO DO?" \ "Start Docker Desktop, wait for Docker to start, and try again." exit 1 fi else echo-error "Unable to talk to docker daemon." \ "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/troubleshooting/common-issues/" exit 1 fi fi } check_docksal_running () { check_docker_running if ! is_docksal_running; then echo-notice "Some Docksal services were not running" "Restarting Docksal system services..." sleep 1 system_reset fi } # Ensures that path is accessible to Docker # @param $1 - path to check (if empty checks `pwd`) check_docker_path () { local _path="${1:-$(pwd)}" if ! is_docker_path "$_path"; then echo-error "The path is not accessible in Docker" \ "Could not access ${yellow}$_path${NC}" \ "It is not shared from your host to Docker or is restricted." exit 2 fi } # Checks if path is accessible to Docker (takes 1.2s on average) # @param $1 - path to check (fails if empty) is_docker_path () { local _path="${1}" [[ "$_path" == "" ]] && return 1 (cd "$_path" 2>/dev/null && docker run --rm -v "$_path":"$_path" busybox 2>/dev/null) } # Check VirtualBox version # Return value 0: VirtualBox installed and the version is OK # Return value 1: VirtualBox not installed # Return value 2: VirtualBox installed, but needs to be updated is_vbox_version () { ! which "$vboxmanage" >/dev/null 2>&1 && return 1 # Trim \r from output (necessary on Windows) local virtualbox_version=$("$vboxmanage" -v | sed "s/r.*//" 2>/dev/null | tr -d '\r') [[ $(ver_to_int "$virtualbox_version") < $(ver_to_int "$REQUIREMENTS_VBOX") ]] && \ return 2 return 0 } # Print Docker Desktop version docker_desktop_version () { if is_mac; then echo $(defaults read /Applications/Docker.app/Contents/Info.plist CFBundleShortVersionString) 2>/dev/null fi if is_wsl; then # Query Windows registry for the "Docker Desktop" app "DisplayVersion" powershell.exe 'Get-ItemPropertyValue "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\Docker Desktop" DisplayVersion' fi } is_docker_version () { # Skip the check on GitHub Actions/Play-with-Docker/Katacoda ( is_github_actions || is_pwd || is_katacoda || is_gitpod ) && return # Rely on our own docker binary, except on Linux if is_linux; then ! which docker &>/dev/null && return 1 else ! [[ -x ${DOCKER_BIN} ]] && return 1 fi # Trim \r from output (necessary on Windows) local version=$(docker version --format '{{.Client.Version}}' 2>/dev/null | tr -d '\r') (( $(ver_to_int "$REQUIREMENTS_DOCKER") <= $(ver_to_int "$version") )) } is_docker_server_version () { # Skip the check on GitHub Actions/Play-with-Docker/Katacoda ( is_github_actions || is_pwd || is_katacoda || is_gitpod ) && return # Rely on our own docker binary, except on Linux if is_linux; then ! which docker &>/dev/null && return 1 else ! [[ -x ${DOCKER_BIN} ]] && return 1 fi # Override version requirement for Docker Desktop if is_docker_native; then REQUIREMENTS_DOCKER=${REQUIREMENTS_DOCKER_DD} fi # Override version requirement for VirtualBox/boot2docker if is_boot2docker; then REQUIREMENTS_DOCKER=${REQUIREMENTS_DOCKER_B2D} fi # Trim \r from output (necessary on Windows) local version=$(docker version --format '{{.Server.Version}}' 2>/dev/null | tr -d '\r') (( $(ver_to_int "$REQUIREMENTS_DOCKER") <= $(ver_to_int "$version") )) } is_docker_compose_version () { ! [[ -x ${DOCKER_COMPOSE_BIN} ]] && return 1 # Trim 'v' prefix from output # Trim \r from output (necessary on Windows) local version=$(docker-compose version --short | tr -d 'v' | tr -d '\r') (( $(ver_to_int "$REQUIREMENTS_DOCKER_COMPOSE") <= $(ver_to_int "$version") )) } is_docker_machine_version () { ! [[ -x ${DOCKER_MACHINE_BIN} ]] && return 1 # Trim \r from output (necessary on Windows) local version=$(docker-machine -v | sed "s/.*version \(.*\),.*/\1/") (( $(ver_to_int "$REQUIREMENTS_DOCKER_MACHINE") <= $(ver_to_int "$version") )) } is_winpty_version () { ! [[ -x ${WINPTY_BIN} ]] && return 2 # Trim \r from output (necessary on Windows) local version=$("$WINPTY_BIN" --version | head -1 | sed "s/.*version \(.*\).*/\1/" | tr -d '\r') (( $(ver_to_int "$REQUIREMENTS_WINPTY") <= $(ver_to_int "$version") )) } is_proxy_sshagent () { [[ "$DOCKSAL_SSH_AGENT_USE_HOST" == "1" ]] && ! is_linux } wants_rc_version () { [[ "${DOCKSAL_USE_RC}" == 1 ]] || [[ "${DOCKSAL_USE_RC}" == "true" ]] } # Verbal counterpart to vbox_version # Checks VirtualBox version and prints a message to user if the check failed and why. # Supports skipping version check via SKIP_VBOX_VERSION_CHECK=1 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_github_actions || [[ "$DOCKSAL_CONFIRM_YES" == "1" ]]; 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 } # Display release notes prompt release_notes_message () { echo-notice "Please take a moment to read about what's new in this release:" "${URL_RELEASE_NOTES}" # Open the URL in browser # TODO: add support for Linux and Windows/WSL is_mac && open "${URL_RELEASE_NOTES}" } # Displays sponsorship prompt sponsor_message () { echo-notice "Ever thought about how much time (and $) Docksal saves you and your team every month?" \ "Consider becoming a GitHub Sponsor ❤ to Docksal️. Any contribution is welcome!" \ "${URL_GITHUB_SPONSOR}" # Open the URL in browser # TODO: add support for Linux and Windows/WSL #is_mac && open "${URL_GITHUB_SPONSOR}" } #-------------------------- Containers management ----------------------------- # Connects project containers to the project network _connect_project_network () { local project="${COMPOSE_PROJECT_NAME_SAFE}" local network="${COMPOSE_PROJECT_NAME_SAFE}_default" local containers=$(docker ps -aq --no-trunc --filter "label=com.docker.compose.project=${project}") for container in ${containers}; do docker network connect ${network} ${container} &>/dev/null done } # Disconnects all containers, connected to project network _disconnect_project_network () { local network=${1} local containers=$(docker ps -aq --no-trunc --filter "network=${network}") for container in ${containers}; do docker network disconnect -f "$network" ${container} &>/dev/null done } # This starts/restart the project start. # The order of actions is important in the case of restart, since we manipulate the project network. # Order: (re)create => (re)connect network => start. _start_containers () { check_docker_running # Check that project directory is reachable by Docker. # This helps trace down issues with SMB/NFS file sharing on Mac and Windows. # Note: triggering this check in load_configuration would introduce a blocker for many fin commands. check_docker_path # (Re)create project resources # This will recraete the project network if missing. # docker-compose create &>/dev/null || (echo-error "Failed to create project resources"; return 1) echo-yellow "(Re)creating resources..." docker-compose create --build --no-recreate || return 1 # (Re)connect containers to project network # We have to manually reconnect project containers to project network (after restart). _connect_project_network # TODO: Workaround. Handle this case in docksal/vhost-proxy. # When project stack is started, but vhost-proxy is not yet connected to the stack network, vhost-proxy config breaks. # To mitigate this we should connect vhost-proxy to the project stack network ASAP. _vhost_proxy_connect # Start project containers # Suppress docker pull details in CI environments otherwise pull prints too much garbage in CI logs. is_ci && quiet_pull="--quiet-pull" || quiet_pull="" echo-yellow "Starting services..." docker-compose up -d --remove-orphans --build ${quiet_pull} || return 1 _healthcheck_wait } # @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-yellow "Stopping services..." docker-compose stop "$@" # TODO: Do we still need this? # While Docker Compose v2 recreates the project network if one does not exist, # it does not reconnect project containers to the project network. # If we delete the project network, we have to then manually reconnect project containers to the project network. # See _connect_project_network if [[ $1 == '-a' ]] || [[ $1 == '--all' ]] || [[ -z $1 ]]; then echo-yellow "Disconnecting project network..." # There is a limit of about 30 docker networks per host, so unused (stopped projects') networks should be dropped. # The project network will be automatically recreated by docker-compose up. local network="${COMPOSE_PROJECT_NAME_SAFE}_default" _disconnect_project_network "${network}" docker network rm "${network}" &>/dev/null || true fi } _restart_containers () { _stop_containers "$@" && sleep 1 && _start_containers } # @param $1 container_name _remove_containers () { check_project_environment if [[ $1 == "" ]]; then echo-yellow "Removing resources..." # Disconnect all containers from the project network, otherwise network will not be removed. # Figure out the default project network name local network="${COMPOSE_PROJECT_NAME_SAFE}_default" _disconnect_project_network "${network}" # 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 "$@" && # For cli and db, we have to manually drop the volumes, since named volumes are omitted by the "-v" option above { for i in "$@"; do [[ "$i" == "cli" ]] && docker volume rm -f "${COMPOSE_PROJECT_NAME_SAFE}_cli_home" >/dev/null [[ "$i" == "db" ]] && docker volume rm -f "${COMPOSE_PROJECT_NAME_SAFE}_db_data" >/dev/null done return 0 } fi } # Remove unused Docker images, and unused Docksal related volumes and containers cleanup () { check_docker_running # Orphaned project stacks are containers/volumes that map to non-existing codebases on the host # E.g., the codebase was removed without doing proper "fin project rm" first echo-green "Checking for orphaned Docksal project stacks..." 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 project containers and volumes..." docker ps -qa --filter "label=com.docker.compose.project=${project_name}" | xargs docker rm -f # Remove project volumes. "docker volume prune" (used below) does not remove the project_root volume. # See https://github.com/moby/moby/issues/40152 docker volume ls -q --filter "label=com.docker.compose.project=${project_name}" | xargs docker volume rm -f fi done # Most dangling volumes should be cleaned up by the orphaned stacks cleanup. # Cleanup any leftover dangling volumes created by Docksal. echo-green "Removing dangling Docksal volumes..." docker volume prune -f --filter "label=io.docksal" 2>/dev/null echo-green "Removing dangling images..." docker image prune -f 2>/dev/null # TODO: use the filter once we have the filter added to all Docksal images (will take a while) #docker image prune -f --filter "label=io.docksal" 2>/dev/null } # Image cleanup wizard cleanup_images () { check_docker_running echo-green "Docker images cleanup Wizard" echo sleep 1 # Print all images first echo "Printing all existing images:" sleep 1 docker images --format="{{.Repository}}:{{.Tag}}" | sort echo "---------------------------------------------------" echo "| Use the list above to help you cleanup |" echo "---------------------------------------------------" # Declare an arary of names and fill it with the output from the docker command declare -a images images+=($(docker images --format="{{.Repository}}:{{.Tag}}+{{.ID}}" | sort)) # Loop over array KEYS ${!array[@]} to be able to use the key to lookup id in array of ids local image_name; local image_id; for image in "${images[@]}"; do image_name=$(echo "$image" | cut -d "+" -f 1) image_id=$(echo "$image" | cut -d "+" -f 2) if _confirm "Remove ${yellow}$image_name${NC} ($image_id) ?" --no-exit; then docker image remove "$image_id" fi done } # Cleans up ANY (not only Docksal managed): # - stopped containers # - dangling volumes cleanup_hard () { check_docker_running if [[ "$(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}" if _confirm "Continue?" --no-exit; then echo -e "${NC}" echo-green "Removing stopped containers..." docker container prune -f 2>/dev/null fi echo -e "${NC}" fi dangling_volumes="$(docker volume ls -qf dangling=true)" if [[ "$dangling_volumes" != "" ]]; then echo -e "${red}WARNING: ${yellow}Preparing to delete the following dangling volumes:${NC}" echo "$dangling_volumes" printf '–%.0s' $(seq 1 40) echo -e "${yellow}" if _confirm "Continue?" --no-exit; then echo -e "${NC}" echo-green "Removing dangling volumes..." echo "$dangling_volumes" | xargs docker volume rm -f 2>/dev/null fi echo -e "${NC}" fi echo-green "Removing all dangling images..." docker image 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 } # 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 _healthcheck_wait () { local delay=5 # Delay (seconds) before rerunning the check local timeout=${DOCKSAL_HEALTHCHECK_TIMEOUT} # Total time (seconds) before the healthcheck is considered failed local elapsed=0 local status="" local FILTER="label=com.docker.compose.project=${COMPOSE_PROJECT_NAME}" sleep 1 # Small sleep to give quick containers time to start status=$(docker ps --filter "${FILTER}" --format '{{ .Status }}') # While one or more containers are starting wait for them while [[ "${status}" =~ "(health: starting)" ]]; do echo "Waiting for project stack to become ready..." sleep ${delay}; elapsed=$((elapsed + delay)) if ((elapsed > timeout)); then echo-error "Project healthcheck has timed out" \ "One or more containers did not enter a healthy state within ${timeout} seconds of the project start." \ "See ${yellow}fin project status${NC} for details" \ "Check ${yellow}fin logs${NC} for more details" return 1 fi status=$(docker ps --filter "${FILTER}" --format '{{ .Status }}') done # All containers have started, check the final status if [[ "${status}" =~ "(health: unhealthy)" ]]; then echo-error "One or more containers are not healthy" \ "See ${yellow}fin project status${NC} for details" "This is most likely due to a project misconfiguration." return 1 fi 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-green "Docksal command line utility${NC} (${yellow}v${FIN_VERSION}${NC})" echo echo -e "${lightmagenta}Docksal Docs:${NC} https://docs.docksal.io/" echo -e "${lightmagenta}Sponsor ❤ Docksal:${NC} ${URL_GITHUB_SPONSOR}" echo echo-green "Usage:" printh "fin [command]" echo echo-green "Management Commands:" printh "addon " "Addons management commands: install, remove (${yellow}fin help addon${NC})" "yellow" printh "alias " "Manage aliases that allow ${yellow}fin @alias${NC} execution (${yellow}fin help alias${NC})" "yellow" printh "db " "Manage databases (${yellow}fin help db${NC})" "yellow" printh "hosts " "Hosts file commands: add, remove, list (${yellow}fin help hosts${NC})" "yellow" printh "project " "Manage project(s) (${yellow}fin help project${NC})" "yellow" printh "ssh-key " "Manage SSH keys (${yellow}fin help ssh-key${NC})" "yellow" printh "system " "Manage Docksal state (${yellow}fin help system${NC})" "yellow" if is_boot2docker || [[ "$TERM" == "dumb" ]]; 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 "config [command]" "Show or change configuration (${yellow}fin help config${NC})" printh "exec " "Execute a command or a script in ${yellow}cli${NC}" printh "exec-url " "Download script from URL and run it on host (URL should be public)" printh "init" "Initialize a project (override it with your own automation, see ${yellow}fin help init${NC})" printh "image " "Image management commands: registry, save, load (${yellow}fin help image${NC})" printh "logs [service]" "Show service logs (e.g., Apache logs, MySQL logs) and Unison logs (${yellow}fin help logs${NC})" printh "pull [options]" "Commands for interacting with Hosting Providers (${yellow}fin help pull${NC})" printh "run-cli (rc) " "Run a command in a standalone cli container in the current directory (${yellow}fin help run-cli${NC})" printh "share" "Create temporary public url for current project using ngrok" printh "share-v2" "Create a temporary public URL for the project using Cloudflare Tunnel" printh "vhosts" "List all virtual *.docksal hosts registered in Docksal proxy" echo echo-green "Docker command wrappers" printh "docker (d) " "Run Docker commands directly" printh "docker-compose (dc) " "Run Docker Compose commands directly" printh "docker-machine (dm) " "Run Docker Machine commands directly" echo echo-green "Misc. command wrappers" printh "composer " "Run Composer commands" printh "drush " "Drush command (requires Drupal)" printh "drupal " "Drupal Console command (requires Drupal 8)" printh "platform " "Platform.sh's CLI (requires docksal/cli 2.3+)" printh "terminus " "Pantheon's Terminus (requires docksal/cli 2.1+)" printh "wp " "WordPress CLI command (requires WordPress)" echo echo-green "Diagnostics/maintenance/updates" printh "cleanup [options]" "Remove all unused Docker images, unused Docksal volumes and containers" printh "diagnose" "Show diagnostic information for troubleshooting and bug reporting" printh "sysinfo" "Show system information" printh "update [options]" "Update Docksal" printh "version (--version, v, -v)" "Print fin version. [v, -v] prints short version" # Do not show addons and custom commands if help for fin only is requested (needed for docs generation) if [[ "$1" == "fin" ]]; then return; fi # 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 "Installed 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 found:" 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 } # 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 -L "$addons_path" -maxdepth 4 -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}\//} local cmd_path=${cmd_name} local cmd_short=${cmd_name##*/} local cmd_regex="${cmd_short}/${cmd_short}$" [[ ${cmd_name} =~ $cmd_regex ]] && cmd_name=${cmd_name/${cmd_short}\/${cmd_short}/${cmd_short}} # command description is lines that start with ## local filename="$addons_path/$cmd_path" 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_exec () { echo echo "Execute commands or script in \`cli\` service container," echo "or execute commands in other containers when specified in params." echo "fin exec will automatically cd into the same folder inside \`cli\`." echo echo "Usage: exec [-T] [--in=name] " echo " [!] Parameters order matters." 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 or container 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 volumes" 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 " --repo=name" "Clone from a custom repo: name (--choice is set to '0' automatically)" printh " --branch=name" "Clone from a custom repo: branch name (optional)" 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" printh "fin project create --name=myproject --repo=https://github.com/org/project.git" printh "" "Initialize project from a custom git repo" } show_help_reset () { show_help_project } show_help_rm () { show_help_project } show_help_remove () { show_help_project } show_help_ssh-key() { echo echo "Manage SSH keys loaded into Docksal" echo printh "Private SSH keys loaded into the secure docksal-ssh-agent service are accessible to all project containers." printh "This allows containers to connect to the external SSH servers that require SSH keys" printh "without a need to copy over the key into the container every time." printh "Default keys id_rsa/id_dsa/id_ecdsa/id_ed25519 are loaded automatically on every project start." echo echo "Usage: fin ssh-key [params]" echo echo "Commands:" printh "add [key-name] [--quiet]" "Add a private SSH key from \$HOME/.ssh by file name" printh "" "Adds all default keys (id_rsa/id_dsa/id_ecdsa/id_ed25519) if no file name is given." printh "" "Suppress key already loaded notifications if --quiet option specified." printh "ls" "List SSH keys loaded in the docksal-ssh-agent" printh "rm" "Remove all keys from the docksal-ssh-agent" printh "new [key-name]" "Generate a new SSH key pair" echo echo "Examples:" printh "fin ssh-key add" "Loads all SSH keys with default names: id_rsa/id_dsa/id_ecdsa from \$HOME/.ssh/" printh "fin ssh-key add server_rsa" "Loads the key stored in \$HOME/.ssh/server_id_rsa into the agent" printh "fin ssh-key new server2_rsa" "Generates a new SSH key pair in ~/.ssh/server2_id_rsa" } 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 "--system-images" "Update system images" printh "--project-images" "Update project images" printh "--self" "Update fin executbale" printh "--tools" "Update tools" printh "--stack" "Update config files" printh "--bash-complete" "Install bash completions" echo echo "Examples:" printh "fin update --project-images" "Pull the latest project specific images" printh "DOCKSAL_UPDATE_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" "Forcibly 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 "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 "Create a temporary public URL for the project using 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_share-v2 () { echo echo "Create a temporary public URL for the project using Cloudflare Tunnel" echo echo "Usage: share-v2 [command]" echo echo "Commands:" printh "start" "Start tunnel and print public URL" printh "stop" "Stop tunnel" printh "status" "Prints tunnel status and public URL (if Active)" printh "url" "Prints tunnel public URL" printh "logs" "Prints tunnel container logs" echo echo "Examples:" printh "fin share-v2 start" "Start tunnel and print public URL" } 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 "yml" "Display static YML project config suitable for export (NOTE: SECRET_* values will not be hidden)" echo 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" echo 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" 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" 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" 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" printh "fin config set DOCKSAL_STACK=acquia" " Set different default stack in .docksal/docksal.env" printh "fin config set --env=local XDEBUG_ENABLED=1" "Enable XDEBUG in .docksal/docksal-local.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/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" " Addons repo" } show_help_cleanup () { echo echo "Remove all unused Docker images, and Docksal related volumes and orphaned containers to save disk space." echo "Orphaned are those containers which project folders were deleted from the filesystem," echo "but the containers still linger in the Docker." echo echo "Usage: cleanup [options]" echo echo "Options:" printh "--images" "Docker images cleanup Wizard" printh "--hard" "Remove ALL stopped containers even unrelated to Docksal (potentially destructive operation)" echo echo "Examples:" printh "fin cleanup" "Regular cleanup" printh "fin cleanup --images" "Docker Images cleanup wizard, helping to navigate and delete unused ones" printh "fin cleanup --hard" "Run hard cleanup removing all stopped Docker containers" } 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 -n "$FIN_VERSION" else echo "Docksal version: $DOCKSAL_VERSION" echo "fin version: v$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 if [ -d "$project_commands_path/$cmd_name" ]; then for sub_cmd_name in $(ls "$project_commands_path/$cmd_name" 2>/dev/null | tr "\n" " "); do [[ -f "$project_commands_path/$cmd_name/$sub_cmd_name" ]] && custom_commands+=" $cmd_name/$sub_cmd_name" done else custom_commands+=" $cmd_name" fi 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-keys 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 ----------------------------------- # Get machine status from docker machine. # This is an expensive operation (~1.5s) and should be used when relying on the cached status is not good enough. # DOCKER_MACHINE_STATUS is set at the beginning of runtime using this function. docker_machine_status () { echo $(docker-machine status "$DEFAULT_MACHINE_NAME" 2>/dev/null || echo 'Defunct' ) } is_docker_machine_exist () { is_docker_machine_version || return 1 # Refresh machine status # This is necessary to make sure we don't get the cached value after operations with the VM. DOCKER_MACHINE_STATUS=$(docker_machine_status) [[ "$DOCKER_MACHINE_STATUS" != "Defunct" ]] } is_docker_machine_running () { # Check the cached value set when docker-machine state is altered [[ "$DOCKER_MACHINE_STATUS" == 'Running' ]] } # Create docker machine # param $1 machine name docker_machine_create () { check_vbox_version check_binary_found 'docker-machine' if is_docker_machine_exist; then echo-error "Docker Machine '$DEFAULT_MACHINE_NAME' already exists" return 1 fi echo-green "Creating docker machine '$DEFAULT_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 # Use 2 CPUs in case of 4 or more CPUs/cores on host # This tremendously helps with network files system operations in some cases local VIRTUALBOX_CPU_COUNT=1 system_cpu_count=$(getconf _NPROCESSORS_ONLN 2>/dev/null) # process possible failure as 1 cpu system_cpu_count=${system_cpu_count:-1} if (( ${system_cpu_count} > 3 )); then VIRTUALBOX_CPU_COUNT=2 fi # Allow Docksal subnet IPs to be assigned to a VirtualBox host-only adapter (VirtualBox v6.1.28+) # https://github.com/docksal/docksal/issues/1596 if is_mac && ! (cat /etc/vbox/networks.conf 2>/dev/null | grep -e "^\* ${DOCKSAL_SUBNET}" >/dev/null); then echo "Writing /etc/vbox/networks.conf..." sudo mkdir -p /etc/vbox echo "* ${DOCKSAL_SUBNET}" | sudo tee -a /etc/vbox/networks.conf >/dev/null fi docker-machine create \ --driver=virtualbox \ --virtualbox-boot2docker-url "${URL_BOOT2DOCKER}" \ --virtualbox-cpu-count "$VIRTUALBOX_CPU_COUNT" \ --virtualbox-disk-size "${VBOX_HDD:-$DEFAULT_MACHINE_VBOX_HDD}" \ --virtualbox-memory "$DEFAULT_MACHINE_VBOX_RAM" \ --virtualbox-hostonly-cidr "$DOCKSAL_SUBNET" \ --virtualbox-no-share \ "$DEFAULT_MACHINE_NAME" if [[ $? == 0 ]]; then # Update cached values DOCKER_MACHINE_STATUS="Running" # Note docker is not yet accessible here until docker_machine_env is called later else return 1 fi } docker_machine_env () { local _env local res #remove cache rm -f "$CONFIG_DOCKER_MACHINE_ENV" 2>/dev/null # get the env if is_wsl; then # In WSL, docker-machine is a Windows binary, but WSL does not understand Windows paths _env=$(docker-machine env --shell=bash docksal | sed "s/\\\\/\//g" | sed "s/C:/\/c/") else _env=$(docker-machine env --shell=bash "$DEFAULT_MACHINE_NAME") fi 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_DOCKER_MACHINE_ENV" >/dev/null } # Stop docker machine docker_machine_stop () { check_binary_found 'docker-machine' docker-machine stop "$DEFAULT_MACHINE_NAME" # Manually update cached values DOCKER_MACHINE_STATUS="Stopped" DOCKER_RUNNING="false" } docker_machine_start () { check_binary_found 'docker-machine' if [[ "$DOCKER_MACHINE_STATUS" == "Stopped" ]]; then docker-machine start "$DEFAULT_MACHINE_NAME" if_failed_error "Failed starting virtual machine" docker_machine_env if_failed_error "Failed to properly access virtual machine" \ "In case you see certificates problem, try rebooting your local host." \ "Common issues: https://docs.docksal.io/troubleshooting/common-issues/" # Update cached values # We would not get here if something above failed, so a blind update is fine (saves 1.5s) DOCKER_MACHINE_STATUS="Running" # Once machine is up, is_docker_running check should be instant (no savings in doing a blind update) # docker_machine_env has to happen before this check or the check will fail is_docker_running && DOCKER_RUNNING="true" elif [[ "$DOCKER_MACHINE_STATUS" == "Defunct" ]]; then docker_machine_create && docker_machine_env # Once machine is up, is_docker_running check should be instant (no savings in doing a blind update) # docker_machine_env has to happen before this check or the check will fail is_docker_running && DOCKER_RUNNING="true" # Displays an error message and offers to remove the vm if something failed if [[ $? != 0 ]]; then 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 fi fi } docker_machine_remove () { # Do not check for machine existence here. # In certain cases (e.g. machine create failed), the VirtualBox VM may not exist, but there would be a broken # machine record in docker-machine (dm ls). _confirm "Remove $DEFAULT_MACHINE_NAME?" if is_docker_machine_exist; then # Store host-only interface name before removing the VM. local vboxifname=$("$vboxmanage" showvminfo "$DEFAULT_MACHINE_NAME" 2>/dev/null | grep "Host-only" | sed "s/.*Host-only.*'\(.*\)'.*/\1/g") fi docker-machine rm -f "$DEFAULT_MACHINE_NAME" 2>/dev/null # Manually update cached values DOCKER_MACHINE_STATUS="Defunct" DOCKER_RUNNING="false" if [[ "${vboxifname}" != "" ]]; then # After VM is 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}" 2>/dev/null # Killing the network interface also helps to avoid issues when VM is recreated. "$vboxmanage" hostonlyif remove "$vboxifname" 2>/dev/null # Remove old DHCP leases because VirtualBox 6 retains then which breaks docker machine IP re-creation is_mac && rm -f "$HOME/Library/VirtualBox/HostInterfaceNetworking-${vboxifname}-Dhcpd.leases" is_wsl && rm -f "$WIN_HOME/.VirtualBox/HostInterfaceNetworking-${vboxifname}-Dhcpd.leases" fi } # Create a NFS export # @param $@ - host path to be shared (accepts multiple values separated by space) nfs_share_add () { [[ "$@" = "" ]] && return 1 eval $(parse_params "$@") # Ensure /etc/exports exists and nfsd is enabled # nfsd won't start or even do "checkexports" without /etc/exports present if [[ ! -f /etc/exports ]]; then echo "Creating /etc/exports..." sudo touch /etc/exports fi # Start nfsd if it's not running if ! (sudo nfsd status >/dev/null 2>&1); then echo "Starting nfsd..." # Give nfsd a moment to start sudo nfsd start >/dev/null; sleep 1 fi # Set nfs.server.mount.require_resv_port = 0 # Many NFS server implementations require this because of the false belief that this requirement increases security. # NFS mounts are very likely to fail without this. if ! (cat /etc/nfs.conf | grep -e '^nfs\.server\.mount\.require_resv_port\s*=\s*0' >/dev/null); then echo "Writing /etc/nfs.conf..." echo "nfs.server.mount.require_resv_port = 0" | sudo tee -a /etc/nfs.conf >/dev/null # Need to stop and start nfsd because macOS 14.4 broke the restart command sudo nfsd stop &>/dev/null # Give nfsd a moment to start sudo nfsd start >/dev/null; sleep 1 fi # 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_export in ${ARGV[@]}; do if [[ ! -d ${share_export} ]]; then echo-error "Invalid path: ${share_export}" return 1 fi # With VirtualBox, connections originate from ${DOCKSAL_IP} # With Docker Desktop using HyperKit, connections originate from 127.0.0.1 or ${DOCKSAL_HOST_IP} (alias on lo0) # With Docker Desktop using Virtualization Framework or on Apple Silicone, connections originate from the 192.168.81.0/24 network or ${DOCKSAL_HOST_IP} # We can put them all in the same export line exports="${exports}$share_export 127.0.0.1 ${DOCKSAL_HOST_IP} 192.168.81.0 -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 if [[ "$exports_errors" =~ "conflicts" ]]; then return 2 # conflict error else echo-error "There is an error in /etc/exports file" \ "Read through the error message(s) below and fix errors in ${yellow}/etc/exports${NC} manually" \ "Then run ${yellow}fin system reset${NC}" echo "-----------------" echo "$exports_errors" echo "-----------------" return 1 # other error fi fi # Write and apply /etc/exports settings echo "Writing /etc/exports..." echo -e "$exports" | sudo tee /etc/exports >/dev/null # Give nfsd a moment to update exports list sudo nfsd update; sleep 1 else # /etc/export already contains what is required echo "NFS shares are already configured" fi # Check nfsd status and exports nfsd status showmount -e } # Removes all Docksal NFS exports nfs_share_remove () { # Remove our own old exports local exports_open="# /dev/null) local exports_new=$(cat /etc/exports 2>/dev/null | \ tr "\n" "\r" | \ sed "s/${exports_open}.*${exports_close}//" | \ tr "\r" "\n" ) # Return if there are no changes [[ "${exports_new}" == "${exports}" ]] && return # Write temporary exports file to /tmp/etc.exports.XXXXX and check it local exports_test="/tmp/etc.exports.$RANDOM" echo -e "$exports_new" | 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_new" echo "-----------------" return 2 fi # Write and apply /etc/exports settings echo "Writing /etc/exports..." echo -e "$exports_new" | sudo tee /etc/exports >/dev/null # Give nfsd a moment to update exports list sudo nfsd update; sleep 1 # Check nfsd status and exports nfsd status showmount -e } # Mount folder inside docker-machine with NFS # Unnamed params - shares in format "$LOCAL_FOLDER:$MOUNT_POINT_NAME" docker_machine_mount_nfs () { local nfs_ip="$DOCKSAL_HOST_IP" eval $(parse_params "$@") # Mount exported folders echo-green "Mounting NFS shares..." # Start NFS client on docker-machine docker-machine ssh "$DEFAULT_MACHINE_NAME" "sudo /usr/local/etc/init.d/nfs-client start" >/dev/null for share in ${ARGV[@]}; do local share_=(${share//:/ }); local share_export="${share_[0]}" local share_mount="${share_[1]}" echo "Mounting local $share_export to $share_mount" # [!!] Think twice about performance before changing nfs settings nfs_mount_command="sudo mount -t nfs -o vers=3,nolock,noacl,nocto,noatime,nodiratime,actimeo=1 $nfs_ip:$share_export $share_mount" # Timeout will break mount command, that can freeze if connection to host is blocked by firewall docker-machine ssh "$DEFAULT_MACHINE_NAME" \ "sudo mkdir -p $share_mount; timeout -t 5 sudo umount $share_mount 2>/dev/null; timeout -t 20 $nfs_mount_command" if [[ $? != 0 ]]; then echo "Retrying mounting local $share_export to $share_mount" sleep 5 docker-machine ssh "$DEFAULT_MACHINE_NAME" "timeout -t 20 $nfs_mount_command" if [[ $? != 0 ]]; then echo-error "NFS share mount failed." "Try ${yellow}fin vm mount${NC} or ${yellow}fin vm restart${NC}." return 1 fi fi done } # Fix/optimize Windows network settings for file sharing. # See http://serverfault.com/a/236393 for details. # TODO: Deprecated and soon to be removed. # This was applicable to Windows 7. Most likely irrelevant at this point. 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 is_wsl && USERNAME="$(wsl_env USERNAME)" is_wsl && USERDOMAIN="$(wsl_env USERDOMAIN)" # Add SMB share if it does not exist. if [[ "$(net.exe share)" != *"${share_name}"* ]]; then echo-green "Adding docker SMB share ${share_name}..." if is_wsl; then sudowin net.exe share "${share_name}=${share_path}" "/grant:${USERDOMAIN}\\${USERNAME},FULL" "/REMARK:Docksal" else command_share="net.exe share ${share_name}=${share_path} /grant:${USERDOMAIN}\\${USERNAME},FULL /REMARK:Docksal" winsudo "$command_share" fi if [[ $? != 0 ]]; then echo-error "SMB share creation failed." \ "Share name: ${share_name}" \ "Share path: ${share_path}" return 1 fi fi } # Removes Docksal SMB shares smb_share_remove () { # Get a list of logical drives (type 3 = Local disk). local drives=$(wmic.exe logicaldisk get drivetype,name | grep 3 | awk '{print tolower($2)}' | sed 's/[:\r]//g') for drive in ${drives}; do # Avoid conflicts with existing drive shares by using a unique share name local share_name="docksal-$drive" if is_wsl; then sudowin net share ${share_name} /DELETE else command_share="net.exe share ${share_name} /DELETE" winsudo "$command_share" fi done } # 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//\'/} is_wsl && USERNAME="$(wsl_env USERNAME)" is_wsl && USERDOMAIN="$(wsl_env USERDOMAIN)" # 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}',vers=3.02,nobrl,mfsymlinks,noperm,actimeo=1,dir_mode=0777,file_mode=0777 //${DOCKSAL_HOST_IP}/$share_name $mount_point" docker-machine ssh ${DEFAULT_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 ${DEFAULT_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 mount${NC} to try again." \ "For troubleshooting instructions see ${yellow}https://docs.docksal.io/troubleshooting/windows-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 ... 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 mount${NC}" return 1 fi # Get a list of logical drives (type 3 = Local disk). local drives=$(wmic.exe logicaldisk get drivetype,name | grep 3 | awk '{print tolower($2)}' | sed 's/[:\r]//g') for drive in ${drives}; do # Mount point is lowercase drive letter with no slash # Avoid conflicts with existing drive shares buy using a unique share name local share_name="docksal-$drive" # echo smb_share_add "$share_name" "$drive" smb_share_add "$share_name" "$drive:" && smb_share_mount "$share_name" "/$drive" "$password" done } # Creates bind mounts on WSL from /mnt/c to /c so that docker-compose path 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 if ! (echo "$mounts" | grep " on /$drive " >/dev/null); then echo "Creating bind mount /mnt/$drive -> /$drive" sudo mkdir -p "/$drive" sudo mount --bind "/mnt/$drive" "/$drive" fi done } #------------------------------- VM Commands ----------------------------------- vm () { if is_docker_native; then echo-error "Docksal VM cannot be used in Docker Desktop mode" \ "You have DOCKER_NATIVE set to 1. VM functions should not be used in this mode." \ "See https://docs.docksal.io/getting-started/docker-modes/ for details" 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 $DEFAULT_MACHINE_NAME does not exist. Use 'fin system start'." return 1 fi case $1 in start) # Show a message and pause for input. echo -e "Deprecated. Please use ${yellow}fin system start${NC} instead." read -p "Press any key to continue..." system_start ;; restart) system_stop system_start ;; status) docker-machine status ${DEFAULT_MACHINE_NAME} ;; stop) # Show a message and pause for input. echo -e "Deprecated. Please use ${yellow}fin system stop${NC} instead."; read -p "Press any key to continue..." system_stop ;; ssh) shift docker-machine ssh ${DEFAULT_MACHINE_NAME} "$@" ;; stats) vm-stats ;; kill) system_stop docker-machine kill ${DEFAULT_MACHINE_NAME} # Manually update cached values DOCKER_MACHINE_STATUS="Defunct" DOCKER_RUNNING="false" ;; ls|list) docker-machine ls ;; remove|rm) system_stop docker_machine_remove ${DEFAULT_MACHINE_NAME} ;; mount) configure_mounts ;; ip) docker-machine ip ${DEFAULT_MACHINE_NAME} ;; env) docker-machine env --shell=bash ${DEFAULT_MACHINE_NAME} ;; ram) shift vm-ram "$@" ;; hdd) shift vm-hdd "$@" ;; help) show_help_vm ;; regenerate-certs) docker-machine regenerate-certs -f ${DEFAULT_MACHINE_NAME} vm restart ;; *) show_help_vm return 1 ;; esac } vm-ram () { echo "Total $("$vboxmanage" showvminfo "$DEFAULT_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 "$DEFAULT_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 "$DEFAULT_MACHINE_NAME" df -h /mnt/sda1 } #------------------------------- Other Commands ----------------------------------- # Start containers up () { check_docksal_environment # Check that project name is unique since non-unique names will give problems with docker-compose check_project_unique # Run "ssh_key add" here, since there is no better place to get it triggered on Linux and Docker for Mac/Win # Use --quiet here, otherwise this becomes annoying # Note: this takes ~1.5s, so we only call it when absolutely necessary ( ! is_proxy_sshagent && ( is_linux || is_docker_native ) ) && ssh_key add --quiet # 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 project containers _start_containers || return 1 # If Unisons is used, wait for it to sync unison_sync_wait # TODO: maybe move these into a function and call in other places - reset/restart. echo-green "Project URL: ${yellow}${PROJECT_URL}${NC}" if [[ "${IDE_ENABLED}" == "1" ]]; then echo-green "Web IDE URL: ${yellow}${PROJECT_IDE_URL}${NC}" fi } # 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 # Check that project name is unique since non-unique names will give problems with docker-compose check_project_unique # Run "ssh_key add" here, since there is no better place to get it triggered on Linux and Docker for Mac/Win # Use --quiet here, otherwise this becomes annoying # Note: this takes ~1.5s, so we only call it when absolutely necessary ( ! is_proxy_sshagent && ( is_linux || is_docker_native ) ) && ssh_key add --quiet # Fix project root permissions if necessary set_project_root_permissions # Restart project containers _restart_containers "$@" # If Unisons is used, wait for it to sync 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-warning "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 system" local ret=$? [[ "$ret" != 0 ]] && return ${ret} # Run "ssh_key add" here, since there is no better place to get it triggered on Linux and Docker for Mac/Win # Use --quiet here, otherwise this becomes annoying # Note: this takes ~1.5s, so we only call it when absolutely necessary ( ! is_proxy_sshagent && ( is_linux || is_docker_native ) ) && ssh_key add --quiet # Fix project root permissions if necessary set_project_root_permissions # Start project containers _start_containers || return 1 # If Unisons is used, wait for it to sync unison_sync_wait } # List project containers project_status () { check_docksal_environment check_project_unique docker-compose ps -a } # 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" # Read repo from params if available if [[ "$repo" != "" ]]; then local choice="0" local target_repo="$repo" # Read branch from params if available local target_branch [[ "$branch" != "" ]] && target_branch="$branch" fi # 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 10 (Composer Version)" echo -e "${green} 2.${NC} Drupal 10 (BLT 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 -e "${green} 13.${NC} Angular" echo echo -e "${magenta} HTML${NC}" echo -e "${green} 14.${NC} Static HTML site" echo echo -e "${red} Custom${NC}" echo -e "${green} 0.${NC} Custom git repository" echo echo -e "" # TODO: UPDATE THIS VARIABLE IF YOU ADD MORE CHOICES! local choices=14 while [[ "$choice" == "" ]]; do read -p "Enter your choice (0-$choices): " choice if ! (( "$choice" >= "0" && "$choice" <= $choices )); then choice="" echo-error "Enter a number between 0 and $choices." fi done fi # Set target repo case ${choice} in 1) target_cms="Drupal 10 (Composer Version)" target_host_name_search_string="drupal9.docksal" target_repo="$URL_REPO_DRUPAL10COMPOSER" ;; 2) target_cms="Drupal 10 (BLT Version)" target_host_name_search_string="drupal9.docksal" target_repo="$URL_REPO_DRUPAL10BLT" ;; 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="Angular" target_host_name_search_string="" target_repo="$URL_REPO_ANGULAR" ;; 14) target_cms="Plain HTML" target_host_name_search_string="" target_repo="" ;; 0) target_cms="Custom git repository" target_host_name_search_string="" if [[ "$target_repo" == "" ]]; then while true; do read -p "Please enter repository url: " target_repo [[ "${target_repo}" == "" ]] && continue break done fi ;; esac echo echo -e "Project folder: ${yellow}$(pwd)/$project_name${NC}" echo -e "Project software: ${yellow}${target_cms}${NC}" echo -e "Source repo: ${yellow}${target_repo}${NC}" echo -e "Source branch: ${yellow}${target_branch:-}${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 \ $( [[ "${target_branch}" != "" ]] && echo "--branch=${target_branch}" ) \ "${target_repo}" "${project_name}" --depth=1 --no-tags [[ $? != 0 ]] && _confirm "Checkout finished with errors. Do you wish to continue with project init?" cd "${project_name}" # Edit docksal.env to use a custom user-supplied host name if [[ -f ".docksal/docksal.env" ]]; then sed -i~ "s/VIRTUAL_HOST=${target_host_name_search_string}/VIRTUAL_HOST=${project_virtual_host_name}/g" .docksal/docksal.env fi echo echo -e "${yellow}3. Passing execution to ${yellow_bold}fin init${yellow}...${NC}" echo 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-warning "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 () { # Configure network has to happen before any communications with the docker daemon happen (before check_docker_running) if [[ "$1" == "" ]] ; then configure_network configure_mounts echo-green 'Resetting Docksal system 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" == "mounts" ]] ; then configure_mounts 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 system_start () { # Reset network settings before doing anything else (just in case) # (May happen if switching modes native vs not). system_reset network # This will instruct the user what to do if Docker daemon is not accessible check_docker_running --quiet # This will do the rest of the configuration, assuming the VM/Docker are already running. system_reset } # Stops/disables all Docksal things system_stop () { if is_boot2docker; then # Stop the docker-machine VM. if is_docker_machine_running; then echo-green "Stopping Docksal VM..." docker_machine_stop fi else # Stop all Docksal project and system containers. # 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" 2>/dev/null | xargs docker rm -vf >/dev/null fi stop_sshproxy_daemon configure_resolver off configure_mounts off configure_network off } # Helper function to run ssh-key inside the ssh-agent container _ssh_key () { # Return if no key or option passed [[ "$@" == "" ]] && return if ! is_tty || is_github_actions ; 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." docker exec docksal-ssh-agent ssh-key "$@" else # Run ssh-key interactively to allow entering a passphrase for ssh keys (if set) ${winpty} docker exec -it docksal-ssh-agent ssh-key "$@" fi } # Generates a new SSH key pair with a given name ssh_key_generate () { local ssh_path="$HOME/.ssh" # On Windows docker and docker-compose expect a Windows style path is_windows && ssh_path=$(cygpath -w "$ssh_path") ssh_key_name="${1}" # If no key name provided, ask for it [[ "${ssh_key_name}" == "" ]] && read -p "Enter name for a new SSH key (e.g., myserver_id_rsa): " ssh_key_name # Exit if still no key name here [[ "${ssh_key_name}" == "" ]] && exit 1 ssh_key_path="${ssh_path}/${ssh_key_name}" echo "Generating a new 4096 bit RSA key..." ssh-keygen -t rsa -b 4096 -f ${ssh_key_path} -q -N "" chmod 600 ${ssh_key_path} echo "Private key file: ${ssh_key_path}" echo "Public key file: ${ssh_key_path}.pub" echo echo "Contents of the public SSH key (copy and use this as an authorized key on a remote system):" echo && cat ${ssh_key_path}.pub && echo } # Manage SSH keys in the ssh-agent ssh_key () { check_winpty_found # Parse parameters and remove options from parameters line eval $(parse_params "$@") set -- ${ARGV[@]} # Check if we support key management if [[ "$DOCKSAL_SSH_AGENT_USE_HOST" == "1" ]]; then if [[ "${quiet}" == "" ]]; then echo-warning 'Docksal is currently not managing SSH keys.' \ "Please use ${yellow}ssh-add${NC} command to manage SSH keys in the host's ssh-agent." fi return 1 fi # 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 alert the user and ask them to try a manual fix. return 1 fi if [[ "${1}" != "add" ]]; then _ssh_key "$@" return $? 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") # If the user does not have any SSH keys (no ~/.ssh folder), offer to generate a default key if [[ ! -d "$ssh_path" ]]; then echo-yellow "You don't seem to have any SSH keys in the default location ('${ssh_path}')." _confirm "Would you like to generate a default SSH key pair now?" --no-exit || return # Ensure ssh_path exists mkdir -p "$ssh_path" # Generate a default id_rsa key ssh_key_generate id_rsa fi # Copy all keys from the host to the ssh-agent container. # This is a safer method compared to passing individual keys via stdin. # Takes 600ms vs 200ms for the stdin approach. Is faster when passing 4+ keys. docker cp "$ssh_path/." docksal-ssh-agent:/.ssh >/dev/null # If we have a key name as the argument, add it to the list of keys to load if [[ "${2}" != "" ]]; then if [[ -f "${ssh_path}/${2}" ]]; then local SECRET_SSH_KEY_${2}="${2}" else echo-error "Key '"${ssh_path}/${2}"' does not exist" fi # Load default keys else # When ssh-key add is called without arguments it should load the default keys [[ -f "${ssh_path}/id_rsa" ]] && local SECRET_SSH_KEY_ID_RSA=id_rsa [[ -f "${ssh_path}/id_dsa" ]] && local SECRET_SSH_KEY_ID_DSA=id_dsa [[ -f "${ssh_path}/id_ecdsa" ]] && local SECRET_SSH_KEY_ID_ECDSA=id_ecdsa [[ -f "${ssh_path}/id_ed25519" ]] && local SECRET_SSH_KEY_ID_ED25519=id_ed25519 fi # Load SSH keys into the agent # This is a unified approach, which handles default keys as well as keys passed as an argument eval 'ssh_keys=(${!SECRET_SSH_KEY_@})' for ssh_key in "${ssh_keys[@]}" do local ssh_key_name=${!ssh_key} local ssh_key_path="${ssh_path}/${ssh_key_name}" if [[ -f ${ssh_key_path} ]]; then # Pass the key into the ssh-agent container (~200ms vs ~600ms for docker cp) # TODO: without a public key present, ssh-agent asks for a passphrase (if set) twice. #cat ~/.ssh/${ssh_key_name} | ${winpty} docker exec -i docksal-ssh-agent bash -c "cat /dev/stdin > /.ssh/${ssh_key_name}" # Call "ssh-key add" in the ssh-agent container [[ "${quiet}" == "" ]] && _ssh_key add ${ssh_key_name} || _ssh_key add ${ssh_key_name} --quiet # Remove copied key #docker exec docksal-ssh-agent rm -f /.ssh/${ssh_key_name} # # Handle everything in one docker exec # # TODO: this does not work with keys with a passphrase # cat ~/.ssh/${ssh_key_name} | ${winpty} docker exec -i docksal-ssh-agent bash -c "\ # set -e; \ # cat /dev/stdin > /.ssh/${ssh_key_name}; \ # ssh-key add ${ssh_key_name}; \ # rm -f /.ssh/${ssh_key_name}; \ # " else echo-error "Key '${ssh_key_name}' does not exist." fi done # Remove copied keys from the ssh-agent container after loading into the ssh-agent. # Commented out in favor of passing individual keys on stdin of the agent container. docker exec docksal-ssh-agent rm -rf '/.ssh/*' } #----- 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_wsl || 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' # Gitpod instance is_gitpod && source="Gitpod" 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 } # Checks whether local network interface is configured for Docksal check_network_alpine () { grep -q "lo:docksal" <<< "$(ip addr show dev lo)" } # Configure Docksal network settings on Alpine via ip addr configure_network_alpine () { local mode="${1:-on}" # Determine current status local status check_network_alpine && 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 } # Checks whether the local network interface is configured for Docksal. # Applicable to Docker Desktop only. check_network_mac () { local settings=$(ifconfig lo0) grep -q "${DOCKSAL_IP} netmask" <<< ${settings} || return 1 grep -q "${DOCKSAL_HOST_IP} netmask" <<< ${settings} || return 1 } # Configure Docksal network settings on Mac configure_network_mac () { local mode="${1:-on}" # Determine current status local status check_network_mac && 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 } # Checks whether DOCKER_WIN_NETWORK network interface is configured for Docksal # Applicable to Docker Desktop only. check_network_windows () { # Trim \r from output (necessary on Windows) local settings=$(netsh.exe interface ip show address name="${DOCKER_WIN_NETWORK}" | tr -d '\r') grep -q "${DOCKSAL_IP}$" <<< ${settings} || return 1 grep -q "${DOCKSAL_HOST_IP}$" <<< ${settings} || return 1 } # Configure Docksal network settings on Windows configure_network_windows () { local mode="${1:-on}" # Determine current status local status check_network_windows && status="enabled" || status="disabled" # Docker for Win if [[ "$mode" == "on" && "$status" == "disabled" ]] ; then # Add Docksal IP aliases on the DockerNAT adapter local command1="netsh.exe interface ip add address name=\"${DOCKER_WIN_NETWORK}\" addr=${DOCKSAL_IP} mask=255.255.255.0" local command2="netsh.exe interface ip add address name=\"${DOCKER_WIN_NETWORK}\" addr=${DOCKSAL_HOST_IP} mask=255.255.255.0" winsudo "$command1 & $command2" elif [[ "$mode" == "off" && "$status" == "enabled" ]]; then # Remove Docksal IP aliases on the DockerNAT adapter local command1="netsh.exe interface ip delete address name=\"${DOCKER_WIN_NETWORK}\" addr=${DOCKSAL_IP}" local command2="netsh.exe interface ip delete address name=\"${DOCKER_WIN_NETWORK}\" addr=${DOCKSAL_HOST_IP}" winsudo "$command1 & $command2" fi # Wait 5s for network configuration to apply sleep 5 } # Checks whether DOCKER_WIN_NETWORK network interface is configured for Docksal # Applicable to Docker Desktop only. check_network_wsl () { # Trim \r from output (necessary on Windows) local settings=$(netsh.exe interface ip show address name="${DOCKER_WIN_NETWORK}" | tr -d '\r') grep -q "${DOCKSAL_IP}$" <<< ${settings} || return 1 grep -q "${DOCKSAL_HOST_IP}$" <<< ${settings} || return 1 } # Configure Docksal network settings on WSL configure_network_wsl () { local mode="${1:-on}" local status check_network_wsl && status="enabled" || status="disabled" # Docker for Win if [[ "$mode" == "on" && "$status" == "disabled" ]] ; then # Add Docksal IP aliases on the DockerNAT adapter sudowin netsh.exe interface ip add address name=\"${DOCKER_WIN_NETWORK}\" addr="${DOCKSAL_IP}" mask="255.255.255.0" sudowin netsh.exe interface ip add address name=\"${DOCKER_WIN_NETWORK}\" addr="${DOCKSAL_HOST_IP}" mask="255.255.255.0" elif [[ "$mode" == "off" && "$status" == "enabled" ]]; then # Remove Docksal IP aliases on the DockerNAT adapter sudowin netsh.exe interface ip delete address name=\"${DOCKER_WIN_NETWORK}\" addr="${DOCKSAL_IP}" sudowin netsh.exe interface ip delete address name=\"${DOCKER_WIN_NETWORK}\" addr="${DOCKSAL_HOST_IP}" fi # Wait 5s for network configuration to apply sleep 5 } # Configure Docksal network settings configure_network () { local mode="${1:-on}" # Always set mode to "off" when running on Mac/Windows/WSL in VirtualBox mode. # Mode "on" is only applicable to Linux and Docker for Mac/Windows. if ! is_docker_native && (is_mac || is_windows || is_wsl); then mode="off" fi echo-green "Configuring network settings..." is_gitpod && return 0 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 $? } # Configure Docksal mounts configure_mounts () { local mode="${1:-on}" # Create and mount network shares from host to VM if [[ "$mode" == "on" ]]; then if is_mac; then echo-green "Configuring NFS shares..." nfs_share_add "${DOCKSAL_NFS_PATH}" res=$? # Exit 2 = conflict or other error in /etc/exports. Try to auto-resolve it. if [[ $res == 2 ]]; then # TODO: possibly, print a note that anything above the configured DOCKSAL_NFS_PATH is invisible to the VM. echo-green "Possible NFS conflict detected. Attempting auto-fix:" _confirm "Suggested path for Docksal projects: ${yellow}${HOME}/Projects${NC}. Try it?" DOCKSAL_NFS_PATH="${HOME}/Projects" mkdir -p "${DOCKSAL_NFS_PATH}" # Write proposed value to the global Docksal config config_set --global DOCKSAL_NFS_PATH="${DOCKSAL_NFS_PATH}" # Configure exports using the new value nfs_share_add "${DOCKSAL_NFS_PATH}" res=$? if [[ $res == 2 ]]; then echo-error "There is still an NFS conflict" \ "With ${yellow}${DOCKSAL_NFS_PATH}${NC} your ${yellow}/etc/exports${NC} would still contain a conflict." \ "You need to manually review your ${yellow}/etc/exports${NC} and resolve conflicts." \ "Most likely remove the lines that export folders under ${yellow}${DOCKSAL_NFS_PATH}${NC}" \ "Then run ${yellow}fin system reset${NC}" exit 1 elif [[ $res != 0 ]]; then echo-error "NFS sharing has failed" exit 1 fi elif [[ $res != 0 ]]; then echo-error "NFS sharing has failed" exit 1 fi # Mount the share in VirtualBox # TODO: Drop this default NFS mount? (v2.0) # NFS mounts are now handled using NFS volumes at the project level - same approach as with Docker Desktop. if ! is_docker_native; then docker_machine_mount_nfs "${DOCKSAL_NFS_PATH}:${DOCKSAL_NFS_PATH}" fi fi # Create, then mount shares in VirtualBox if (is_wsl || is_windows) && ! is_docker_native; then echo-green "Configuring SMB shares..." docker_machine_mount_smb fi else if is_mac; then echo-green "Removing NFS exports..." nfs_share_remove fi if (is_wsl || is_windows) && ! is_docker_native; then echo-green "Removing SMB shares..." smb_share_remove fi fi } # Start system-wide vhost-proxy service install_proxy_service () { docker rm -f docksal-vhost-proxy >/dev/null 2>&1 || true # TODO: Clean this up in 2023 docker volume rm docksal_projects >/dev/null 2>&1 || true # Open up vhost-proxy to the world in CI/Sandbox/PWD environments ( is_ci || is_pwd || is_katacoda || is_gitpod ) && DOCKSAL_VHOST_PROXY_IP="0.0.0.0" # Enable auto-start of projects by default in CI environments is_ci && PROJECT_AUTOSTART=${PROJECT_AUTOSTART:-1} # Mount custom certs folder if available local mount_certs if [[ -d "${CONFIG_CERTS}" ]] && is_docker_path "${CONFIG_CERTS}"; then mount_certs="--mount type=bind,src=${CONFIG_CERTS},dst=/etc/certs/custom" fi # Mount projects directory local mount_projects if [[ -d "${PROJECTS_ROOT}" ]] && is_docker_path "${PROJECTS_ROOT}"; then mount_projects="--mount type=bind,src=${PROJECTS_ROOT},dst=/projects" fi # PROJECT_INACTIVITY_TIMEOUT, PROJECT_DANGLING_TIMEOUT and PROJECTS_ROOT 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! # setting PROJECT_AUTOSTART to 1 will enable project autostart by visiting project url docker run -d --name docksal-vhost-proxy --label "io.docksal.group=system" --restart=unless-stopped \ -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}" \ -e PROJECT_AUTOSTART="${PROJECT_AUTOSTART:-0}" \ -e DEFAULT_CERT="${DOCKSAL_VHOST_PROXY_DEFAULT_CERT:-docksal}" \ --mount type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \ ${mount_certs} \ ${mount_projects} \ --log-opt max-size=${DOCKSAL_CONTAINER_LOG_MAX_SIZE} \ --log-opt max-file=${DOCKSAL_CONTAINER_LOG_MAX_FILE} \ --health-interval=${DOCKSAL_CONTAINER_HEALTHCHECK_INTERVAL} \ "${IMAGE_VHOST_PROXY}" >/dev/null if_failed_error "Failed starting the proxy service." } # 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" break fi done else # In case no name servers found, fall back to the default DOCKSAL_DNS_UPSTREAM="$DOCKSAL_DEFAULT_DNS" fi elif is_windows || is_wsl; then # Look for Primary WINS server, it represents DNS whe WINS services is in use local primary_wins=$(ipconfig.exe /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]} fi # If previous detection failed, try another one. Lists all DNS addresses. Uses first non-localhost # Not an ideal check as it might pick the wrong one if there are several, but better than nothing if [[ "$DOCKSAL_DNS_UPSTREAM" == "" ]]; then dns_addresses="" # We need IFS to only be newline for the next for loop local _IFS=${IFS} IFS=$'\n' for dns_found in $(netsh.exe interface ip show config | grep DNS | grep -v None | grep -v $DOCKSAL_IP); do # Trim \r from output (necessary on Windows) dns_addresses="${dns_addresses} "$(echo "$dns_found" | cut -d ":" -f 2 | tr -d '\r' | xargs) done # Restore IFS back to default IFS=${_IFS} for dns in $dns_addresses; do if [[ ! "$dns" =~ "127.0."* ]]; then DOCKSAL_DNS_UPSTREAM="${dns}" break fi done fi fi # If detected upstream points to localhost or is empty, then fall back to the default if [[ "$DOCKSAL_DNS_UPSTREAM" =~ "127.0."* || "$DOCKSAL_DNS_UPSTREAM" == "" ]]; then DOCKSAL_DNS_UPSTREAM="$DOCKSAL_DEFAULT_DNS" fi # export to variable and return (for direct usage or debug) export DOCKSAL_DNS_UPSTREAM } # Start system-wide dns service install_dns_service () { # Skip/disable if built-in DNS is disabled if [[ "$DOCKSAL_DNS_DISABLED" == "1" ]]; then docker rm -f docksal-dns >/dev/null 2>&1 || true return fi 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=unless-stopped \ -p "${DOCKSAL_DNS_IP:-$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 \ --log-opt max-size=${DOCKSAL_CONTAINER_LOG_MAX_SIZE} \ --log-opt max-file=${DOCKSAL_CONTAINER_LOG_MAX_FILE} \ --health-interval=${DOCKSAL_CONTAINER_HEALTHCHECK_INTERVAL} \ "${IMAGE_DNS}" >/dev/null if_failed_error "Failed starting the DNS service." } # Configure system-wide *.docksal resolver using /etc/resolver configure_resolver_mac () { local mode="${1:-on}" # Global DNS resolver kill switch [[ "$DOCKSAL_NO_DNS_RESOLVER" != "0" ]] && mode="off" 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_DEFAULT" >/dev/null 2>&1 sudo rm -r "/etc/resolver/$DOCKSAL_DNS_DOMAIN" >/dev/null 2>&1 fi # Flush DNS cache echo-green "Clearing DNS cache..." # On macOS clearing DNS cache is a bit tricky and needs to be done in several places. # Note: sometimes neither is needed, other times - one or both. sudo dscacheutil -flushcache # Flush DNS cache sudo killall -HUP mDNSResponder # Reload mDNSResponder config # to check: run "scutil --dns" and look for the "docksal" domain resolver at the bottom of the list. } # 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" # Global DNS resolver kill switch [[ "$DOCKSAL_NO_DNS_RESOLVER" != "0" ]] && mode="off" # 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" local metric=10 # 10 is used increase the adapter's priority. 75 to - deprioritize it # Global DNS resolver kill switch [[ "$DOCKSAL_NO_DNS_RESOLVER" != "0" ]] && mode="off" # 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="${DOCKER_WIN_NETWORK}" elif is_docker_machine_exist; then # Need to check whether machine exists, otherwise we cannot get its info # Figure out the name of the network (not the name of the adapter) the VM is using network_id=$("$vboxmanage" showvminfo ${DEFAULT_MACHINE_NAME} --machinereadable | grep hostonlyadapter | cut -d'"' -f2 | sed 's/Ethernet Adapter/Network/') else # There is nothing to configure (machine and thus its network interface does not exist) return 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" local metric=10 # 10 is used increase the adapter's priority. 75 to - deprioritize it # Global DNS resolver kill switch [[ "$DOCKSAL_NO_DNS_RESOLVER" != "0" ]] && mode="off" # 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="${DOCKER_WIN_NETWORK}" elif is_docker_machine_exist; then # Need to check whether machine exists, otherwise we cannot get its info # Figure out the name of the network (not the name of the adapter) the VM is using network_id=$("$vboxmanage" showvminfo ${DEFAULT_MACHINE_NAME} --machinereadable | grep hostonlyadapter | cut -d'"' -f2 | sed 's/Ethernet Adapter/Network/') else # There is nothing to configure (machine and thus its network interface does not exist) return fi # Set DNS server to Docksal IP on the VM's VBox adapter sudowin netsh.exe 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 sudowin netsh.exe 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 () { local mode="${1:-on}" # Global DNS resolver kill switch [[ "$DOCKSAL_NO_DNS_RESOLVER" != "0" ]] && mode="off" 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 if is_alpine; then # nslookup on Alpine does not support the '-timeout' option timeout 5 nslookup google.com "$DOCKSAL_DNS_UPSTREAM" 2>/dev/null | grep google.com >/dev/null else nslookup -timeout=1 google.com "$DOCKSAL_DNS_UPSTREAM" 2>/dev/null | grep google.com >/dev/null fi 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 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 # Switch between built-in agent and auth socket proxying local ssh_service_args stop_sshproxy_daemon if is_proxy_sshagent; then start_sshproxy_daemon ssh_service_args="ssh-proxy $DOCKSAL_HOST_IP $DOCKSAL_SSH_PROXY_PORT" else ssh_service_args="ssh-agent" fi docker volume create --name docksal_ssh_agent >/dev/null 2>&1 docker run -d --name docksal-ssh-agent --label "io.docksal.group=system" --restart=unless-stopped \ --mount type=volume,src=docksal_ssh_agent,dst=/.ssh-agent \ --log-opt max-size=${DOCKSAL_CONTAINER_LOG_MAX_SIZE} \ --log-opt max-file=${DOCKSAL_CONTAINER_LOG_MAX_FILE} \ --health-interval=${DOCKSAL_CONTAINER_HEALTHCHECK_INTERVAL} \ "${IMAGE_SSH_AGENT}" $ssh_service_args >/dev/null if_failed_error "Failed starting the SSH agent service." # Add default keys. Using || true here to suppress errors if there are no keys on the host. ! is_proxy_sshagent && ssh_key add || true } start_sshproxy_daemon () { if [[ -f "$CONFIG_SSHPROXY_PID" ]]; then echo-error 'SSH proxy is already running.' exit 1 elif ! command -v socat > /dev/null; then echo-error 'SSH proxy requires the "socat" application to function.' if command -v brew > /dev/null; then echo 'Run `brew install socat` to add this application.' elif command -v apt-get > /dev/null; then echo 'Run `apt-get install socat` to add this application.' fi exit 2 elif [[ -z "$SSH_AUTH_SOCK" ]]; then echo-error 'Environment variable SSH_AUTH_SOCK is not set.' \ 'Is your local SSH agent running correctly?' exit 3 fi socat TCP-LISTEN:$DOCKSAL_SSH_PROXY_PORT,bind=$DOCKSAL_HOST_IP,reuseaddr,fork UNIX-CLIENT:$SSH_AUTH_SOCK & local pid=$! if_failed_error "Failed starting the SSH proxy service." echo "$pid" > "$CONFIG_SSHPROXY_PID" } stop_sshproxy_daemon () { local sshproxy_pid=$(cat "$CONFIG_SSHPROXY_PID" 2>/dev/null || echo 0) [[ "$sshproxy_pid" -ne 0 ]] && kill "$sshproxy_pid" 2>/dev/null rm -f "$CONFIG_SSHPROXY_PID" } # Install Mac dependencies install_tools_mac () { # Install Docker client if ! is_docker_version; then local docker_pkg=$(basename "$URL_DOCKER_MAC") echo-green "Downloading 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 -f "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 "Downloading 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 "Downloading 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 } # Install Windows dependencies install_tools_windows () { # Install Docker client if ! is_docker_version; then local docker_pkg=$(basename "$URL_DOCKER_WIN") echo-green "Downloading 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 # docker client binary downloaded from MS (e.g. docker-18-09-2.zip) is dynamically linked and needs DLLs from # the archive. So unpacking everything below, not just docker.exe ( cd "$CONFIG_DOWNLOADS_DIR" && \ unzip -qq ${docker_pkg} && \ mv -f ./docker/* ${CONFIG_BIN_DIR}/ && \ chmod +x ${DOCKER_BIN}.exe ) 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 "Downloading 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 "Downloading 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 "Downloading 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 -f winpty-*/bin/* "$CONFIG_BIN_DIR/" ) if_failed "Check file permissions." fi # Run one-time adjustments during install only. if ! is_update; then # Optimize SMB sharing (deprecated) smb_windows_fix # 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 } # 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 "Downloading 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 version curl -fsSL "${URL_DOCKER_COM}" | VERSION=${REQUIREMENTS_DOCKER} 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 local dc_pkg=$(basename "$URL_DOCKER_COMPOSE_NIX") echo-green "Downloading Docker Compose v${REQUIREMENTS_DOCKER_COMPOSE}..." echo-green "Downloading ${dc_pkg}..." curl -fL# "$URL_DOCKER_COMPOSE_NIX" -o "$DOCKER_COMPOSE_BIN" && \ chmod +x "$DOCKER_COMPOSE_BIN" 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 "Downloading 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_COM}" | 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 local dc_pkg=$(basename "$URL_DOCKER_COMPOSE_NIX") echo-green "Downloading Docker Compose v${REQUIREMENTS_DOCKER_COMPOSE}..." echo-green "Downloading ${dc_pkg}..." curl -fL# "$URL_DOCKER_COMPOSE_NIX" -o "$DOCKER_COMPOSE_BIN" && \ chmod +x "$DOCKER_COMPOSE_BIN" 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 local dc_pkg=$(basename "$URL_DOCKER_COMPOSE_NIX") echo-green "Downloading Docker Compose v${REQUIREMENTS_DOCKER_COMPOSE}..." echo-green "Downloading ${dc_pkg}..." curl -fL# "$URL_DOCKER_COMPOSE_NIX" -o "$DOCKER_COMPOSE_BIN" && \ chmod +x "$DOCKER_COMPOSE_BIN" 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 local dc_pkg=$(basename "$URL_DOCKER_COMPOSE_NIX") echo-green "Downloading Docker Compose v${REQUIREMENTS_DOCKER_COMPOSE}..." echo-green "Downloading ${dc_pkg}..." curl -fL# "$URL_DOCKER_COMPOSE_NIX" -o "$DOCKER_COMPOSE_BIN" && \ chmod +x "$DOCKER_COMPOSE_BIN" if_failed "Docker Compose installation/upgrade has failed." fi } # Install WSL dependencies install_tools_wsl () { # Install Linux Docker client if ! is_docker_version; then local docker_pkg=$(basename "$URL_DOCKER_NIX") echo-green "Downloading Docker client v${REQUIREMENTS_DOCKER}..." if [[ ! -f "$docker_pkg" ]]; then echo-green "Downloading ${docker_pkg}..." curl -fL# "$URL_DOCKER_NIX" -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 -f "docker/docker" "$CONFIG_BIN_DIR/" && \ chmod +x "$DOCKER_BIN" ) if_failed "Check file permissions." fi # Install Linux Docker Compose if ! is_docker_compose_version; then local dc_pkg=$(basename "$URL_DOCKER_COMPOSE_NIX") echo-green "Downloading Docker Compose v${REQUIREMENTS_DOCKER_COMPOSE}..." if [[ ! -f "$dc_pkg" ]]; then echo-green "Downloading ${dc_pkg}..." curl -fL# "$URL_DOCKER_COMPOSE_NIX" -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 Windows Docker Machine client. # The Linux one cannot correctly interact with the Windows VirtualBox cli, eventually resulting in something like: # "Error creating machine: Error in driver during machine creation: write |1: broken pipe" # The Windows binary has to be run from the Windows FS (e.g. from Windows user's profile). local USERPROFILE=$(wsl_path -u $(wsl_env USERPROFILE)) local DOCKER_MACHINE_BIN_WSL="${USERPROFILE}/.docksal/bin/docker-machine.exe" mkdir -p "${USERPROFILE}/.docksal/bin" # Keeping the rest of the logic below very similar to other host environments. if ! is_docker_machine_version; then local dm_pkg=$(basename "$URL_DOCKER_MACHINE_WIN") echo-green "Downloading 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_WSL" && \ chmod +x "$DOCKER_MACHINE_BIN_WSL" 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_WSL" && \ chmod +x "$DOCKER_MACHINE_BIN_WSL" if_failed "Check file permissions." fi fi # Create a symlink to the Windows docker-machine binary inside the WSL FS. ln -sf "$DOCKER_MACHINE_BIN_WSL" "$DOCKER_MACHINE_BIN" } # Installs VirtualBox on Mac/Windows # Picks up a matching vbox version package from ~/Downloads or current directory (preferred). install_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_wsl; then url="$URL_VBOX_WIN" local USERPROFILE=$(wsl_path -u $(wsl_env "USERPROFILE")) vbox_pkg="$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 docker_machine_stop >/dev/null 2>&1 # 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" ) # Install Win/WSL (is_windows || is_wsl) && ( 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 } # Checks VirtualBox version and runs an install if necessary update_virtualbox () { # Install or update VirtualBox if is_boot2docker; then # Check current VirtualBox version is_vbox_version local res=$? if [[ "$res" != "0" ]]; then [[ "$res" == "2" ]] && echo-notice "VirtualBox version should be ${REQUIREMENTS_VBOX} or higher" # Note: install_virtualbox automatically stops the machine if necessary, but will not automatically start it. install_virtualbox fi fi } # Downloads boot2docker.iso, updates existing machine. # Does not automatically create a new machine and starts the updated one. # Dependency: update_tools, update_boot2docker # @option --force-download - forces the boot2docker.iso download update_boot2docker () { # Only applicable to Mac/Windows using VirtualBox/Boot2Docker if is_linux || is_docker_native; then return; fi if is_docker_machine_exist; then # We need the machine up to get it's version if ! is_docker_machine_running; then echo "Starting the existing machine to determine its version..." docker_machine_start fi local b2d_version=$(docker version --format '{{.Server.Version}}') if (( $(ver_to_int "$REQUIREMENTS_DOCKER_B2D") > $(ver_to_int "$b2d_version") )); then if (( $(ver_to_int "$b2d_version") < $(ver_to_int "18.09") )); then # Upgrade to 18.09 requires the system to rebuild due to change from AUFS to Overlay2. echo-warning "This update will destroy and recreate the VM!" \ "You can skip the VM update for now, create DB backups/etc., then restart the update to complete it." \ "For more information see https://docs.docksal.io/troubleshooting/boot2docker-update/" if _confirm "Do you want to update the VM now?" --no-exit; then # Machine is removed here, but not recreated. # It will be started/recreated later in the update process. docker_machine_remove fi else # Perform an upgrade without a rebuild for boot2docker versions 18.09+ docker_machine_stop &>/dev/null # Path to boot2docker.iso for the machine local DEFAULT_MACHINE_ISO_PATH=".docker/machine/machines/${DEFAULT_MACHINE_NAME}/boot2docker.iso" # On Windows machines are stored in the Windows user profile if is_windows || is_wsl; then DEFAULT_MACHINE_ISO_PATH="${WIN_HOME}/${DEFAULT_MACHINE_ISO_PATH}" else # Mac - straightforward DEFAULT_MACHINE_ISO_PATH="${HOME}/${DEFAULT_MACHINE_ISO_PATH}" fi echo-green "Downloading boot2docker.iso v${REQUIREMENTS_DOCKER_B2D}" # Download to temporary location, then move to the permanent location. # This minimizes the chance of getting a corrupt file if the download fails or is interrupted. curl -fL# "${URL_BOOT2DOCKER}" -o "${CONFIG_DOWNLOADS_DIR}/boot2docker.iso" if_failed_error "boot2docker.iso download failed" mv "${CONFIG_DOWNLOADS_DIR}/boot2docker.iso" ${DEFAULT_MACHINE_ISO_PATH} fi fi fi } update_tools () { if is_linux; then # The automated get.docker.com installation method does not work properly in GitHub Actions (ubuntu). # We have to rely on the version preinstalled in the runner. is_github_actions && install_tools_generic_linux && return $? 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=" $STACKS_OVERRIDES_CLOUDFLARED $STACKS_OVERRIDES_GITPOD $STACKS_OVERRIDES_DD_BIND $STACKS_OVERRIDES_IDE $STACKS_OVERRIDES_XHPROF $STACKS_SERVICES $STACKS_STACK_ACQUIA $STACKS_STACK_DEFAULT $STACKS_STACK_DEFAULT_NODB $STACKS_STACK_NODE $STACKS_STACK_PANTHEON $STACKS_STACK_PLATFORMSH $STACKS_VOLUMES_BIND $STACKS_VOLUMES_NFS $STACKS_VOLUMES_NONE $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/*" &>/dev/null # Keep get_repo_version_url out of the loop as it may call curl (and slow things down). repo=$(get_repo_version_url) for f in ${files_to_download}; do echo "$(basename ${f})" # exit subshell with error if download failed curl -kfsSLO "${repo}/stacks/$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" sudo 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" sudo 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 () { 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 # no system_reset here, just the image pull } # 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 } # Updates all docksal images present on the host update_docksal_images () { check_docker_running for _image in $(docker images --format '{{.Repository}}:{{.Tag}}' | grep docksal/ | sort | uniq); do docker pull "$_image" done } # Downloads a version of fin to FIN_PATH_UPDATED # Return value 0: new fin version downloaded to FIN_PATH_UPDATED # Return value 2: new fin version is the same as the current version get_fin_updated () { local fin_url="$(get_repo_version_url)/bin/fin" 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 "=") 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..." } # If running from $FIN_PATH_UPDATED then it is fin.updated run by old fin and we need to skip some steps is_fin_updated () { [[ "$0" == "$FIN_PATH_UPDATED" ]] } # Returns true when running an update # Returns false when installing fin for the first time is_update () { # If there is a timestamp for the last install/update, then we are updating [[ -f ${CONFIG_LAST_UPDATE} ]] } # Update Docksal update () { # Suppress alerts until the very end of the update export DOCKER_VERSION_ALERT_SUPPRESS=1 # [STEP 0] Define total steps TOTAL_STEPS=4 # [STEP 1] Update fin unless already running fin.updated binary if ! is_fin_updated; then testing_warn echo-green "[STEP 1/$TOTAL_STEPS] Updating fin..." # Download new fin version to FIN_PATH_UPDATED get_fin_updated # Run the rest of the update with the newly downloaded fin version. # Pass DOCKSAL_UPDATE_VERSION to keeps versions overrides (if any) consistent. ( DOCKSAL_UPDATE_VERSION=${DOCKSAL_UPDATE_VERSION} "$FIN_PATH_UPDATED" update ) # Catch errors here, otherwise we would exit without errors even "fin update" above failed if_failed_error "Updates failed. fin was not updated." # Overwrite old fin sudo mv -f "$FIN_PATH_UPDATED" "$FIN_PATH" # Must exit here since we've just overwritten the original fin and this may cause bash to choke and through an error. exit fi # [STEP 2] Update stacks echo-green "[STEP 2/$TOTAL_STEPS] Updating stack files..." update_config_files # Reset network settings before doing anything else (just in case) # (May happen if switching modes native vs not). system_reset network # [STEP 3] Update third party tools echo-green "[STEP 3/$TOTAL_STEPS] Updating tools..." update_tools update_virtualbox update_boot2docker # If this is the first time installation, then the current user does not yet have access to docker without sudo # TODO: don't run with sudo if not necessary if is_linux; then # Override docker to run it via sudo docker () { # "command " calls the binary directly ignoring any overriding shell functions # "sudo command ..." does not work, so we have to invoke via path to binary ("command -v ...") sudo $(command -v docker) "$@" } fi # This will instruct the user what to do if Docker daemon is not accessible # --quiet suppresses the user confirmation when starting the existing docker machine check_docker_running --quiet # From now on, we should have everything we need here to start/restart Docksal # [STEP 4] Update Docksal images echo-green "[STEP 4/$TOTAL_STEPS] Updating Docksal images..." # Pull/update system image # While system_reset would also pull images it will do this in a non-interactive way (via docker run). # update_system_images does "docker pull" with does the pull interactively (with a progress indicator), which looks # better. update_system_images # Do cleanup before updating images, otherwise we may be updating images that will be dropped anyway. is_update && cleanup # Update existing docksal images is_update && update_docksal_images # Reset system services after image updates system_reset # Store last successful update timestamp local timestamp=$(date +%s) echo "$timestamp" > "$CONFIG_LAST_UPDATE" echo-green "Update finished" # [STEP - POST UPDATE] # Show release notes URL after update is_update && release_notes_message # Show GitHub Sponsors URL after update is_update && sponsor_message # Cleanup rm -f "$CONFIG_DOWNLOADS_DIR/*" &>/dev/null # Set DOCKSAL_VOLUMES=bind globally for existing Macs (VirtualBox and Docker Desktop) # This ensures we do not break existing project stacks by switching to nfs volumes as the default (Docksal 1.13.0 / fin 1.92.x) if is_update && is_mac && [[ $(config_get --global DOCKSAL_VOLUMES) == "" ]]; then # Until the update process is completed, the old fin binary is still in place, so we can access it # "fin -v" returns the version of the old fin here if [[ $(ver_to_int "$(fin -v)") < $(ver_to_int "1.92.0") ]]; then config_set --global DOCKSAL_VOLUMES=bind fi fi # Rerun docker access/version checks here to inform the users of any issues echo-green "Running post update checks..." # Disabling alert suppression here, since it was likely enabled during earlier calls to check_docker_running unset DOCKER_VERSION_ALERT_SUPPRESS check_docker_running } # Return the latest available Docksal version get_docksal_version_latest () { # Return the requested version when overridden if [[ "${DOCKSAL_UPDATE_VERSION}" != "" ]]; then echo -n ${DOCKSAL_UPDATE_VERSION} return fi # Get all available release tags URL_REPO_RELEASE="${GITHUB_API}/repos/docksal/docksal/releases" tags=$(curl -kfsSL $URL_REPO_RELEASE | grep tag_name | sed 's/.*"tag_name": "\(.*\)",/\1/') if wants_rc_version; then # Return the latest available tag for RC users (final or rc) echo -n $(echo "${tags}" | head -n 1) else # Return the latest final tag for regular users (filter out "rc" tags) echo -n $(echo "${tags}" | grep -v "rc" | head -n 1) fi } # Return a versioned repo URL get_repo_version_url () { echo "${URL_REPO}/$(get_docksal_version_latest)" } 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 [[ "$DOCKSAL_LOCK_UPDATES" == "1" ]] && return # Do not check for updates if disabled 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_repo_version_url)/bin/fin" # 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 -e "You can update after this process by running ${yellow}fin update${NC}" 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\?page_size\=100 | \ 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 () { # Interactive shell requires a tty. if ! is_tty; then echo-error "tty is required to start an interactive shell" return 1 fi if [[ "$1" != "" ]]; then # Pass container name to _exec is specified _exec --in="$1" "__SHELL_INTERACTIVE__" else _exec "__SHELL_INTERACTIVE__" fi } # Run a command in the cli container changing dir to the same folder # @param [-T] Force no-tty # @param [--in=name] Specify container name # @param $@ commands to run _exec () { [[ $1 == "" ]] && show_help_exec && exit check_project_environment check_winpty_found # NOTE: do NOT use parse_params here, it is not sophisticated enough to parse all possible # escapes, quotes and such that can be fed to _exec # 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. if [[ "$1" == "-T" ]]; then local no_tty=true shift fi # Allow overriding container, where to run local container_name="cli" if [[ "$1" =~ "--in" ]]; then container_name=$(echo "$1" | cut -d "=" -f 2) shift fi # Allow entering arbitrary containers by name (e.g., system containers like vhost-proxy). local container_id=$(get_project_container_id "$container_name") if [[ "$container_id" == "" ]]; then container_id="$container_name" fi # Determine shell to run local shell_interactive local shell_noninteractive local container_shell=$(docker inspect --format '{{ index .Config.Labels "io.docksal.shell"}}' ${container_id} 2>/dev/null) if [[ "$container_shell" != "" ]]; then # Use configured shell shell_interactive="$container_shell -ilc" shell_noninteractive="$container_shell -lc" elif [[ "$container_name" == 'cli' ]] || [[ "$container_name" == 'db' ]]; then # For cli and db use bash (complex SQL queries fail when fed from stdin to sh) shell_interactive="bash -ilc" shell_noninteractive="bash -lc" else # Use sh as default shell shell_interactive="sh -ilc" shell_noninteractive="sh -lc" fi # ------------------------------------------------ # # 1) working directory and user # Inside the cli and web containers, start in the same dir # RUN_NO_CDIR can be used to override this (used in mysql_import) local workdir_arg="" if [[ "$container_name" == 'cli' || "$container_name" == 'web' ]] && [[ "$RUN_NO_CDIR" != 1 ]]; then workdir_arg="-w /var/www/$(get_current_relative_path)" fi # User to run commands as. npm and other userspace commands are only available to docker user local user_arg=$(docker inspect --format '{{ index .Config.Labels "io.docksal.user"}}' ${container_id} 2>/dev/null) if [[ "$user_arg" != "" ]]; then user_arg="-u ${user_arg}" elif [[ "$container_name" == "cli" ]]; then # only use docker user for cli by default, others wil use root user_arg="-u docker" fi # ------------------------------------------------ # # ------------------------------------------------ # # 2) convert array of parameters into escaped string # Add space if cmd is not empty # Escape spaces that are "spaces" and not parameter delimiters (i.e., param1 param2\ with\ spaces param3) local cmd if [[ $2 != "" ]]; then cmd=$(printf " %q" "$@") # Do not escape spaces if there is only one parameter (e.g., fin run "ls -la | grep txt") else cmd="${@}" fi # ------------------------------------------------ # # ------------------------------------------------ # # 3) execute # Enter project containers # COLUMNS and LINES have to be passed to workaround a race condition in Docker. See https://github.com/moby/moby/pull/37172#issuecomment-406844485 if [[ "${@}" == "__SHELL_INTERACTIVE__" ]]; then # Special case for running interactive shell with the selected shell # remove the -c switch from the shell_interactive call ${winpty} docker exec -e COLUMNS -e LINES -it ${user_arg} ${workdir_arg} ${container_id} ${shell_interactive/c/} elif 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 ${user_arg} ${workdir_arg} ${container_id} ${shell_interactive} "$cmd; (exit \$?)" else # non-interactive docker exec -e COLUMNS -e LINES ${user_arg} ${workdir_arg} ${container_id} ${shell_noninteractive} "$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:-$IMAGE_RUN_CLI}" # Use unique home volumes for run-cli images # Without this, switching images won't update tools installed in the home volume (like nodejs, etc.). local HOME_VOLUME_NAME="docksal_run_cli_home_$(echo ${RUN_IMAGE} | sed -e 's/[^A-Za-z0-9_.]/-/g')" # 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") elif [[ $1 != "" ]]; then cmd="$cmd $1" # Assume bash if no command was given else cmd="bash -il" fi # 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_PWD if [[ "${DOCKSAL_VOLUMES}" == "nfs" ]]; then # Mounts options here should match those in volumes-nfs.yml # Also, see https://docs.docker.com/storage/volumes/#choose-the--v-or---mount-flag # and "Escape values from outer CSV parser" section on that page MOUNT_PWD="type=volume,dst=/var/www,volume-driver=local,volume-opt=type=nfs,volume-opt=device=:$(pwd),\"volume-opt=o=addr=${DOCKSAL_HOST_IP},vers=3,nolock,noacl,nocto,noatime,nodiratime,actimeo=1\"" else MOUNT_PWD="type=bind,src=$(pwd),dst=/var/www" fi local MOUNT_HOME if [[ "$RUN_CLEAN" != "1" ]]; then # Using "io.docksal" label on the volume ensures the cleanup of run-cli volumes. # Volumes with the "io.docksal" label are dropped by "fin cleanup" (which also runs during updates). docker volume create --label "io.docksal" ${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 # 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 || is_gitpod); 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 ${MOUNT_PWD} \ --mount ${MOUNT_HOME} \ --mount type=volume,src=docksal_ssh_agent,dst=/.ssh-agent,readonly \ --dns=${DOCKSAL_DNS1} \ --dns=${DOCKSAL_DNS2} \ -e BLACKFIRE_CLIENT_ID \ -e BLACKFIRE_CLIENT_TOKEN \ -e SECRET_SSH_PRIVATE_KEY \ -e SECRET_ACQUIA_CLI_KEY \ -e SECRET_ACQUIA_CLI_SECRET \ -e SECRET_TERMINUS_TOKEN \ -e SECRET_PLATFORMSH_CLI_TOKEN \ -e GIT_USER_EMAIL \ -e GIT_USER_NAME \ -e HOST_UID \ -e HOST_GID \ -e NO_PROXY \ -e HTTP_PROXY \ -e HTTPS_PROXY \ -e COLUMNS \ -e LINES \ -e DEBUG \ ${env_str} ${RUN_IMAGE} bash -ilc "$cmd" else # non-interactive docker run --rm \ --mount ${MOUNT_PWD} \ --mount ${MOUNT_HOME} \ --mount type=volume,src=docksal_ssh_agent,dst=/.ssh-agent,readonly \ --dns=${DOCKSAL_DNS1} \ --dns=${DOCKSAL_DNS2} \ -e BLACKFIRE_CLIENT_ID \ -e BLACKFIRE_CLIENT_TOKEN \ -e SECRET_SSH_PRIVATE_KEY \ -e SECRET_ACQUIA_CLI_KEY \ -e SECRET_ACQUIA_CLI_SECRET \ -e SECRET_TERMINUS_TOKEN \ -e SECRET_PLATFORMSH_CLI_TOKEN \ -e GIT_USER_EMAIL \ -e GIT_USER_NAME \ -e HOST_UID \ -e HOST_GID \ -e NO_PROXY \ -e HTTP_PROXY \ -e HTTPS_PROXY \ -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=$(get_project_container_id "db") 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}" local __pty_command='' if is_tty || [[ -z "${ARGV[*]}" ]]; then __pty_command="${winpty}" fi local __pty_alloc='' if is_tty; then __pty_alloc='-t' fi ${__pty_command} docker exec -i ${__pty_alloc} "${container_id}" mysql \ -u"${__dump_user}" \ -p"${__dump_password}" \ ${__database} \ ${ARGV:+-e "${ARGV[*]}"} } # 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=$(get_project_container_id "db") 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 _exec --in="db" "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=$(get_project_container_id "db") 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 -e "Database ${yellow}${__db}${NC} 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=$(get_project_container_id "db") 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 -e "Database ${yellow}${__db}${NC} 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=$(get_project_container_id "db") 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 _exec --in="db" "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=$(get_project_container_id "db") 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 _exec --in="db" "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=$(get_project_container_id "db") __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=$(get_project_container_id "db") 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 container_id=$(get_project_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\|running' | 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}' ${container_name}.${network}:${NGROK_PORT}" fi ${winpty} docker run --rm ${NGROK_VOLUME} -it \ --publish 4040 \ --net ${network} \ --link ${container_name} \ --name ${ngrok_container_name} \ wernight/ngrok \ sh -c "${ARGS}" } # Cloudflare Tunnel (cloudflared) integration # Check whether cloudflared container is running cloudflared_is_running () { # docker ps is about 1s faster than docker-compose ps local container_id=$(docker ps --quiet --filter "name=${COMPOSE_PROJECT_NAME_SAFE}_cloudflared_1") [[ "${container_id}" != "" ]] && return 0 || return 1 } # Get cloudflared container status cloudflared_get_status () { cloudflared_is_running && echo "Active" || echo "Inactive" } # Output cloudflared container logs cloudflared_get_logs () { # docker logs is about 1s faster than docker-compose logs docker logs "${COMPOSE_PROJECT_NAME_SAFE}_cloudflared_1" } # Extracts tunnel host name from cloudflared container logs cloudflared_get_host () { cloudflared_get_logs 2>&1 | grep "trycloudflare.com" | tail -1 | awk '{print $4}' } # Nicely print tunnel URL cloudflared_print_url () { echo -e "${green}Public URL:${NC} ${yellow}$(cloudflared_get_host)${NC}" } # Nicely print tunnel status cloudflared_print_status () { echo -e "${green}Status:${NC} $(cloudflared_get_status)" cloudflared_is_running && echo -e "${green}Public URL:${NC} ${yellow}$(cloudflared_get_host)${NC}" } # Enable and start cloudflared integration cloudflared_start () { config_set --env=local "CLOUDFLARED_ENABLED=1" 1>/dev/null load_configuration 1>/dev/null # Reload stack config after variable update up sleep 10 # Give the tunnel some time to initialize cloudflared_print_url } # Disable and remove cloudflared integration cloudflared_stop () { config_remove --env=local "CLOUDFLARED_ENABLED" 1>/dev/null unset CLOUDFLARED_ENABLED load_configuration 1>/dev/null # Reload stack config after variable update up } # 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 echo "███ DOCKSAL" && version echo 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 echo "Docker Desktop" else echo "VirtualBox VM" fi echo "DOCKER_HOST : $DOCKER_HOST" # NFS settings if is_mac; then echo echo "███ NFS" echo "DOCKSAL_NFS_PATH : $DOCKSAL_NFS_PATH" echo nfsd status echo echo "/etc/exports:" echo "-------------" cat /etc/exports echo "-------------" echo # List configured NFS exports showmount -e echo # List active NFS clients (client:path) showmount -a fi echo echo "███ DOCKER" echo "Expected client version: ${REQUIREMENTS_DOCKER}" if is_linux; then echo "Expected server version: ${REQUIREMENTS_DOCKER}" elif is_docker_native; then echo "Expected server version: ${REQUIREMENTS_DOCKER_DD}" else echo "Expected server version: ${REQUIREMENTS_DOCKER_B2D}" fi echo echo "Installed versions:" echo docker version 2>/dev/null echo echo "███ DOCKER COMPOSE" echo "Expected version: $REQUIREMENTS_DOCKER_COMPOSE" echo "Installed version: $(docker-compose version --short 2>/dev/null)" if [[ "$all" == "all" ]]; then echo echo "███ DOCKER INFO" docker info fi if is_boot2docker; then echo echo "███ DOCKER MACHINE" echo "Expected version: $REQUIREMENTS_DOCKER_MACHINE" echo echo "Installed version:" docker-machine --version 2>/dev/null echo docker-machine ls # Verify the VM IP matches what we expect # If there is a leftover DHCP lease in VritualBox, the VM will be issues a different IP. _vm_ip=$(docker-machine ip ${DEFAULT_MACHINE_NAME}) if [[ "${_vm_ip}" != "${DOCKSAL_IP}" ]]; then echo echo-error "Docksal VM IP mismatch" \ "Expected IP: ${DOCKSAL_IP}" \ "Actual IP : ${_vm_ip}" \ "Try resetting the VM: ${yellow}fin system stop; fin system start${NC}" fi fi if is_docker_running; then echo echo "███ DOCKSAL: PROJECTS" project_list --all echo echo "███ DOCKSAL: VIRTUAL HOSTS" vhosts echo echo "███ DOCKSAL: NETWORKING" echo echo "DOCKSAL_IP: ${DOCKSAL_IP}" echo "DOCKSAL_HOST_IP: ${DOCKSAL_HOST_IP}" echo "DOCKSAL_VHOST_PROXY_IP: ${DOCKSAL_VHOST_PROXY_IP}" echo "DOCKSAL_DNS_IP: ${DOCKSAL_DNS_IP}" echo "DOCKSAL_DNS_DISABLED: ${DOCKSAL_DNS_DISABLED}" echo "DOCKSAL_NO_DNS_RESOLVER: ${DOCKSAL_NO_DNS_RESOLVER}" echo "DOCKSAL_DNS_UPSTREAM: ${DOCKSAL_DNS_UPSTREAM}" echo "DOCKSAL_DNS_DOMAIN: ${DOCKSAL_DNS_DOMAIN}" # Host <> Containers connectivity echo echo "███ DOCKSAL: CONNECTIVITY" echo ## Check that host can access DOCKSAL_IP # Options that work cross-platform: "-c 1" - one request, "-W 1" - wait up to 1s for the first request local PING="ping -c 1 -W 1" echo -n "Host to ${DOCKSAL_IP}: " eval "${PING} ${DOCKSAL_IP}" &>/dev/null [[ $? == 0 ]] && echo-green "PASS" || echo-red "FAIL" ## Check that containers can access DOCKSAL_IP echo -n "Container to ${DOCKSAL_IP}: " docker run --rm busybox sh -c "${PING} ${DOCKSAL_IP}" &>/dev/null [[ $? == 0 ]] && echo-green "PASS" || echo-red "FAIL" ## Check that containers can access DOCKSAL_HOST_IP echo -n "Container to ${DOCKSAL_HOST_IP}: " docker run --rm busybox sh -c "${PING} ${DOCKSAL_HOST_IP}" &>/dev/null [[ $? == 0 ]] && echo-green "PASS" || echo-red "FAIL" # DNS resolution local TEST_URL="http://dns-test.${DOCKSAL_DNS_DOMAIN}" #TEST_CMD="wget -O- ${TEST_URL} &>/dev/null" # Does not work correctly with Alpine's buysbox/wget local TEST_CMD="curl --connect-timeout 2 -sS ${TEST_URL}" echo echo "Checking connectivity to ${TEST_URL}..." ## Test DNS resolution on the host echo -n "Host: " eval ${TEST_CMD} &>/dev/null if [[ $? == 0 ]]; then echo-green "PASS" else echo-red "FAIL" echo echo "Debug info:" echo "----------" set -x cat /etc/resolv.conf | grep ${DOCKSAL_IP} ping -c 1 -W 1 dns-test.docksal nslookup -timeout=1 dns-test.docksal ${DOCKSAL_IP} set +x echo "----------" echo fi ## Test DNS resolution in a container echo -n "Containers: " # TODO: figure out how to use busybox/wget in docksal/empty or use our own version with full wget/curl instead docker run --rm --dns="${DOCKSAL_DNS1}" --dns="${DOCKSAL_DNS2}" curlimages/curl sh -c "${TEST_CMD}" &>/dev/null [[ $? == 0 ]] && echo-green "PASS" || echo-red "FAIL" 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 if is_docker_native; then echo echo "███ DOCKER DESKTOP" echo "EXPECTED VERSION: ${REQUIREMENTS_DOCKER_DESKTOP}" echo "DETECTED VERSION: $(docker_desktop_version)" 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 # Get a list of logical drives (type 3 = Local disk). wmic.exe logicaldisk where drivetype=3 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 DOCKSAL_PATH="$(get_project_path)" export PROJECT_ROOT="$(get_project_path)" is_windows && export PROJECT_ROOT_WIN="$(get_project_path_dc)" } 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 # Reset (as this variables is a list) ENV_FILE="" # 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 # Reset (as this variables is a list) COMPOSE_FILE="" # Project config file 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" [[ "$DOCKSAL_STACK" != "" && ! -f "$stack_yml_file" ]] && echo-error "Incorrect stack definition '$DOCKSAL_STACK'" \ "Stack file does not exist: $stack_yml_file" && exit 1 # 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 volumes by default. if is_mac; then # Default to NFS volumes on Mac (both VirtualBox and Docker Desktop) DOCKSAL_VOLUMES=${DOCKSAL_VOLUMES:-nfs} else # Bind mounted volumes on all other systems DOCKSAL_VOLUMES=${DOCKSAL_VOLUMES:-bind} fi if [[ "$DOCKSAL_VOLUMES" != "disabled" ]]; 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 # Fix bind volumes on Docker Desktop v2.3.0.2+ # See https://github.com/docksal/docksal/issues/1368 overrides_dd_bind_file="$(get_config_dir_dc)/stacks/overrides-dd-bind.yml" if [[ -f "${overrides_dd_bind_file}" ]] && \ is_docker_native && \ (( $(ver_to_int $(docker_desktop_version)) >= $(ver_to_int '2.3.0.2') )) && \ [[ "$DOCKSAL_VOLUMES" == "bind" ]]; then COMPOSE_FILE="${COMPOSE_FILE}${SEPARATOR}${overrides_dd_bind_file}" fi # Enable VSCode (Coder) 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 # Enable xhprof integration overrides_xhprof_file="$(get_config_dir_dc)/stacks/overrides-xhprof.yml" if [[ -f "${overrides_xhprof_file}" ]] && [[ "$XHPROF_ENABLED" != "" ]] && [[ "$XHPROF_ENABLED" != "0" ]]; then COMPOSE_FILE="${COMPOSE_FILE}${SEPARATOR}${overrides_xhprof_file}" fi # Enable Cloudflare Tunnel (cloudflared) integration overrides_cloudflared_file="$(get_config_dir_dc)/stacks/overrides-cloudflared.yml" if [[ -f "${overrides_cloudflared_file}" ]] && [[ "$CLOUDFLARED_ENABLED" != "" ]] && [[ "$CLOUDFLARED_ENABLED" != "0" ]]; then COMPOSE_FILE="${COMPOSE_FILE}${SEPARATOR}${overrides_cloudflared_file}" fi # Enable gitpod integration overrides_gitpod_file="${get_config_dir_d}/stacks/overrides-gitpod.yml" if [[ -f "${overrides_gitpod_file}" ]] && [[ is_gitpod ]]; then COMPOSE_FILE="${COMPOSE_FILE}${SEPARATOR}${overrides_gitpod_file}" fi export COMPOSE_FILE # 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 # Run Docker Compose v2 in compatibility mode. # This forces compose to use underscores instead of hyphens in container names (like in v1). # TODO: Remove this and switch to hyphens in Docksal 2.0. export COMPOSE_COMPATIBILITY=true # 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} elif is_gitpod; then export VIRTUAL_HOST=${DOCKSAL_VHOST_PROXY_PORT_HTTP}-${GITPOD_WORKSPACE_ID}.${GITPOD_WORKSPACE_CLUSTER_HOST} else export VIRTUAL_HOST=${VIRTUAL_HOST:-$COMPOSE_PROJECT_VHOST_NAME_SAFE.$DOCKSAL_DNS_DOMAIN} fi # Display deprecation notice for .docksal TLD. if [[ "$VIRTUAL_HOST" == *.docksal ]]; then echo-warning "'.docksal' base domain and local DNS resolver are deprecated" \ "Please update your Docksal and project configuration" \ "See https://docs.docksal.io/stack/configuration-variables/#docksal-dns-disabled" fi # Make VIRTUAL_HOST lowercase, replace '_' with '-', remove everything non-letter or digit, # make sure it starts with letter or digit local OLD_VHOST=${VIRTUAL_HOST} export VIRTUAL_HOST=$(echo ${VIRTUAL_HOST} | tr '[:upper:]' '[:lower:]' | sed 's/_/-/g' | sed 's/[^-a-z0-9\.]//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 # Create project url if [[ "$DOCKSAL_VHOST_PROXY_PORT_HTTP" == "" || "$DOCKSAL_VHOST_PROXY_PORT_HTTP" == "80" ]]; then export PROJECT_URL="http://${VIRTUAL_HOST}" if [[ "${IDE_ENABLED}" == "1" ]]; then export PROJECT_IDE_URL="http://ide-${VIRTUAL_HOST}" fi else export PROJECT_URL="http://${VIRTUAL_HOST}:${DOCKSAL_VHOST_PROXY_PORT_HTTP}" if [[ "${IDE_ENABLED}" == "1" ]]; then export PROJECT_IDE_URL="http://ide-${VIRTUAL_HOST}:${DOCKSAL_VHOST_PROXY_PORT_HTTP}" fi fi # https://docs.docker.com/compose/reference/envvars/#compose_convert_windows_paths export COMPOSE_CONVERT_WINDOWS_PATHS=1 # Loaded last to allow overrides from env files load_global_configuration } # Default init command. Initializes default config and starts containers init () { if [[ "$(get_project_path)" != "" ]] ; then echo-notice "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 init } 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_@})' # Special case to handle BLACKFIRE variables as secrets # TODO: figure out a way to handle this better # See: https://github.com/docksal/docksal/pull/783#issuecomment-415080823 secrets+=('BLACKFIRE_SERVER_ID') secrets+=('BLACKFIRE_SERVER_TOKEN') secrets+=('BLACKFIRE_CLIENT_ID') secrets+=('BLACKFIRE_CLIENT_TOKEN') for secret in "${secrets[@]}" do local secret_value=${!secret} # Define how many characters to show on the beginning and the end 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 "" echo "MySQL endpoint:" $(docker-compose port db 3306 2>/dev/null | sed "s/0\.0\.0\.0/$DOCKSAL_IP/") echo "Public URL:" $(cloudflared_get_host) # Stop here for "fin config env" [[ "$1" == "env" ]] && return echo echo "Docker Compose configuration" echo "---------------------" docker-compose config [[ $? != 0 ]] && return 1 echo "---------------------" } config_yml () { check_project_root docker-compose config } # TODO: consider deprecating this, since now fin automatically creates docksal.yml and docksal.env (with default stack) config_generate () { eval $(parse_params "$@") if [[ -d ".docksal" ]]; then echo-yellow "A Docksal configuration already exists in this directory" _confirm "Do you want to proceed and overwrite?" fi # Create reset Docksal configuration in the current directory rm -f ".docksal" >/dev/null mkdir -p ".docksal" # Create docksal.env touch ".docksal/docksal.env" && \ echo "DOCKSAL_STACK=${stack:-default}" > ".docksal/docksal.env" # Get docroot option value or use default local DOCROOT="${docroot}" if [[ -z $DOCROOT ]]; then if [[ -d web ]]; then DOCROOT=web elif [[ -d docroot ]]; then DOCROOT=docroot elif [[ -d public_html ]]; then DOCROOT=public_html elif [[ -d html ]]; then DOCROOT=html elif [[ -f index.php ]] || [[ -f index.html ]]; then DOCROOT=. else DOCROOT=docroot fi if ! _confirm "DOCROOT has been detected as ${DOCROOT}. Is that correct?" --no-exit; then DOCROOT="" while [[ "$DOCROOT" == "" ]]; do echo -en "${yellow}Where is your project's DOCROOT?${NC} " read -p "" DOCROOT if [[ ! -d $DOCROOT ]]; then echo-error "${DOCROOT} directory not found." DOCROOT="" fi done fi fi # Add DOCROOT to docksal.env file echo "DOCROOT=${DOCROOT}" >> .docksal/docksal.env # Create a basic init command mkdir -p .docksal/commands && touch .docksal/commands/init && chmod +x .docksal/commands/init cat < .docksal/commands/init #!/usr/bin/env bash fin project rm -f fin project start EOF # 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~ "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~ "/^${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 project's VIRTUAL_HOST value 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 if ! (cat "$HOSTS_FILE" | grep -e "$DOCKSAL_IP $hostname" >/dev/null); then # Always make hosts file backup hosts_backup # Add to hosts echo -e "Adding: ${yellow}$DOCKSAL_IP $hostname${NC}" echo -e "Adding: ${yellow}::2 $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 echo "::2 $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 echo "::2 $hostname" | sudo tee -a "$HOSTS_FILE" >/dev/null fi else echo -e "${yellow}$hostname${NC} already exists in hosts file" fi } # Remove a host from hosts file hosts_remove () { local hostname="$1" # Use project's VIRTUAL_HOST value 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 # Double insurance from removing lines that may damage system if [[ "$hostname" =~ "localhost" ]] || [[ "$hostname" =~ "loopback" ]] || [[ "$hostname" =~ "localnet" ]] || [[ "$hostname" =~ "allnodes" ]]; then echo-error "Cannot remove host" "Removing ${yellow}${hostname}${NC} is not allowed" && exit 1 fi [[ "$hostname" =~ "broadcasthost" ]] && echo-error "Cannot remove broadcasthost" && exit 1 # Always make hosts file backup hosts_backup # Remove from hosts # Drop any lines that include " $hostname" echo -e "Removing ${yellow}${hostname}${NC} host" 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 "$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" != "" ]]; then # Replace addon path as it is on the host, with the matching path inside container container_addon_path="/var/www/${addon/$PROJECT_ROOT\//}" if [[ "$@" != "" ]]; then # Escape spaces that are "spaces" and not parameter delimiters (i.e., param1 param2\ with\ spaces param3) cmd="$container_addon_path "$(printf " %q" "$@") else cmd="$container_addon_path" fi # Run the command _exec --in="$_exec_target" "$cmd" else export ADDON_SCRIPT=true 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" } # Execute specific functions if they exist within the hooks directory. # Examples could be hook_init_pre or hook_db_import. # Specifically look for hooks to run pre and post actions. # Checks project and global .docksal/hooks directory for either a single hook file for specific functions # or checks for individual files that are the hook name. # $1 - name of the hook to search for execute_hook () { local HOOK=$1 # "Executing Hook: ${HOOK}" # Check to see if individual hook file exists. local global_hook_individual_file="${CONFIG_DIR}/hooks/${HOOK}" if [[ -f "${global_hook_individual_file}" ]] && [[ -x "${global_hook_individual_file}" ]]; then # "Executing Global Hook Individual File: ${HOOK}" addon_execute "${global_hook_individual_file}" fi # Check to see if Hooks Exist within the Global Hooks file. local global_hook_group_file="${CONFIG_DIR}/hooks/hooks" if [[ -f "${global_hook_group_file}" ]] && [[ -x "${global_hook_group_file}" ]]; then . ${global_hook_group_file} # Check for help function for specific command type "hook_${HOOK}" >/dev/null 2>&1 if [ $? -eq 0 ]; then # "Executing Global Hook Function: hook_${HOOK}" hook_${HOOK} fi fi # Check to see if individual hook file exists within the project. local project_hook_individual_file="${PROJECT_ROOT}/.docksal/hooks/${HOOK}" if [[ -f "${project_hook_individual_file}" ]] && [[ -x "${project_hook_individual_file}" ]]; then # "Executing Project Hook Individual File: ${HOOK}" addon_execute "${project_hook_individual_file}" fi # Check to see if the global hook file exists within the project. local project_hook_group_file="${PROJECT_ROOT}/.docksal/hooks/hooks" if [[ -f "${project_hook_group_file}" ]] && [[ -x "${project_hook_group_file}" ]]; then . ${project_hook_group_file} # Check for help function for specific command type "hook_${HOOK}" >/dev/null 2>&1 if [ $? -eq 0 ]; then # "Executing Project Hook Function: hook_${HOOK}" hook_${HOOK} fi fi } #-------------------------- RUNTIME STARTS HERE ---------------------------- # Debug mode: print all commands [[ ${FIN_DEBUG} == 1 ]] && set -x # Check host architecture is supported if ! is_supported_arch; then echo-error "Host architecture not supported" \ "Your host architecture is ${yellow}$(host_arch_new)${NC}." \ "Only ${yellow}amd64${NC} and ${yellow}arm64${NC} architectures are supported." # Quit here unless were are in debug mode ! [[ ${FIN_DEBUG} == 1 || "${1}" == "debug" ]] && exit 1 fi # Gitpod Configuration if is_gitpod; then export DOCKSAL_VHOST_PROXY_PORT_HTTP=8080 export DOCKSAL_VHOST_PROXY_PORT_HTTPS=4443 export DOCKSAL_NO_DNS_RESOLVER=1 export DOCKSAL_DNS_DISABLED=1 # Fix the permissions to project directory [[ "$(stat -c "%a" ${GITPOD_REPO_ROOT})" < "755" ]] && echo-warning "${yellow}Fixing Gitpod Repo Root Permissions${NC}" && chmod 755 ${GITPOD_REPO_ROOT} fi # 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 # Inherit hosts git user.email and user.name settings # This happens before the environment variable overrides are sources, thus can be overridden via docksal.env files. # On macOS there is a git wrapper even when git is not installed, so we have to check for git differently on mac # See https://github.com/docksal/docksal/issues/1003 if ! is_mac || (xcode_tools_dir=$(xcode-select -p 2>/dev/null) && [[ -x "${xcode_tools_dir}/usr/bin/git" ]]); then if ( which git &>/dev/null ); then export GIT_USER_EMAIL=$(git config --get --global user.email) export GIT_USER_NAME=$(git config --get --global user.name) fi 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 || is_gitpod); then echo-notice "Running as root is not recommended" fi fi fi # Network settings # Disable internal DNS resolver and use external domain with Docker Desktop for Windows # This is necessary to have a working setup out of the box without the need to ask the user to manually configure DNS # records using "fin hosts". # See https://github.com/docksal/docksal/issues/1276 if is_wsl && is_docker_native; then # Only proceed if DOCKSAL_DNS_DOMAIN was not explicitly set (matches the default) if [[ "${DOCKSAL_DNS_DOMAIN}" == "${DOCKSAL_DNS_DOMAIN_DEFAULT}" ]]; then # Limit this to Docker Desktop for Windows 2.2.0.0+, where it is absolutely necessary if (( $(ver_to_int $(docker_desktop_version)) >= $(ver_to_int '2.2.0.0') )); then export DOCKSAL_NO_DNS_RESOLVER=1 export DOCKSAL_DNS_DOMAIN=docksal.site fi fi fi # Workarounds for Docker Desktop versions 2.2.0.0+, which introduced multiple networking regressions if is_docker_native && (( $(ver_to_int $(docker_desktop_version)) >= $(ver_to_int '2.2.0.0') )); then # Open up vhost-proxy and dns services in Docker Desktop environments # This helps with recurring regressions with binding to an IP in Docker Desktop 2.2.0.0+ DOCKSAL_VHOST_PROXY_IP=${DOCKSAL_VHOST_PROXY_IP:-0.0.0.0} DOCKSAL_DNS_IP=${DOCKSAL_DNS_IP:-0.0.0.0} # Disable internal DNS resolver and use external domain with Docker Desktop for Windows # This is necessary to have a working setup out of the box without the need to ask the user to manually configure DNS # records using "fin hosts". # See https://github.com/docksal/docksal/issues/1276 # Do this only if DOCKSAL_DNS_DOMAIN was not explicitly set (matches the default) is_wsl && [[ "${DOCKSAL_DNS_DOMAIN}" == "${DOCKSAL_DNS_DOMAIN_DEFAULT}" ]] && DOCKSAL_DNS_DISABLED=1 fi # Disable internal DNS resolver and use external domain if docksal-dns is disabled if [[ "$DOCKSAL_DNS_DISABLED" == "1" ]]; then export DOCKSAL_NO_DNS_RESOLVER=1 export DOCKSAL_DNS_DOMAIN=docksal.site fi # DNS resolution settings for containers if [[ "$DOCKSAL_DNS_DISABLED" == "0" ]]; then export DOCKSAL_DNS1=${DOCKSAL_IP} # DNS resolution via docksal-dns (adds support for *.docksal domains) export DOCKSAL_DNS2=${DOCKSAL_DNS_UPSTREAM:-$DOCKSAL_DEFAULT_DNS} # Backup external DNS server else # TODO: get rid of container DNS settings altogether when docksal-dns is disabled export DOCKSAL_DNS1=${DOCKSAL_DNS_UPSTREAM:-$DOCKSAL_DEFAULT_DNS} # External DNS server export DOCKSAL_DNS2='9.9.9.9' # Backup external DNS fi # Create drives bind mounts on wsl # E.g. /mnt/c -> /c wsl_mount_drives # DOCKER_HOST defines how we talk to the docker daemon # Linux - via /var/run/docker.sock (DOCKER_HOST='') if is_linux; then export DOCKER_HOST=''; elif is_docker_native; then if is_wsl; then # WSL + Docker Desktop => connecting via HTTP # The "legacy" (tcp://localhost:2375) connection option in Docker Desktop settings must be enabled. export DOCKER_HOST='127.0.0.1:2375' fi else # Mac/Windows + Docker Machine => connecting via tcp://192.168.64.100:2376 (TLS) (DOCKER_HOST='192.168.64.100:2376') # We rely on docker-machine to configure the connection settings # docker_machine_status is an expensive operation (~1.5s), so we cache it here DOCKER_MACHINE_STATUS=$(docker_machine_status) if [[ "$DOCKER_MACHINE_STATUS" == 'Running' ]]; then # Use cached environment variables if possible if [[ -f "$CONFIG_DOCKER_MACHINE_ENV" ]]; then eval $(cat "$CONFIG_DOCKER_MACHINE_ENV") # if DOCKER_HOST is still not set then get environment variables once again if [[ DOCKER_HOST == "" ]]; then docker_machine_env fi fi fi fi # Allow overriding DOCKER_HOST externally via DOCKSAL_HOST if [[ "$DOCKSAL_HOST" != "" ]]; then export DOCKER_HOST="${DOCKSAL_HOST/tcp:\/\//}" fi # Cache docker status (saves ~200ms) if is_docker_running; then export DOCKER_RUNNING="true" else export DOCKER_RUNNING="false" fi # Default MySQL settings # These may be overridden via project level env files (e.g., docksal.env) export MYSQL_HOST=${MYSQL_HOST:-db} export MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:-root} export MYSQL_USER=${MYSQL_USER:-user} export MYSQL_PASSWORD=${MYSQL_PASSWORD:-user} export MYSQL_DATABASE=${MYSQL_DATABASE:-default} # Container logging settings export DOCKSAL_CONTAINER_LOG_MAX_SIZE export DOCKSAL_CONTAINER_LOG_MAX_FILE # Container healthcheck settings export DOCKSAL_CONTAINER_HEALTHCHECK_INTERVAL # Allow other scripts to source the fin script if [[ "${BASH_SOURCE[0]}" != $0 ]]; then return 0 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 ran within a script if [[ -n $ADDON_SCRIPT ]]; then unset COMPOSE_PROJECT_NAME unset COMPOSE_PROJECT_NAME_SAFE unset COMPOSE_PROJECT_VHOST_NAME_SAFE unset VIRTUAL_HOST fi 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|up) 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|up) 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 ;; pl) # no load_configuration check_for_updates shift project_list "$@" ;; vm) # no load_configuration shift vm "$@" ;; update) if [[ "$DOCKSAL_LOCK_UPDATES" == "1" ]]; then echo-error "Updates locked in this environment" exit 1 fi # no load_configuration shift if [[ "$1" == "--system-images" ]]; then check_docker_running update_system_images system_reset elif [[ "$1" == "--project-images" ]]; then update_project_images elif [[ "$1" == "--self" ]]; then fin_url="$(get_repo_version_url)/bin/fin" 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" [[ $? == 0 ]] && echo "Done" exit elif [[ "$1" == "--tools" ]]; then update_tools elif [[ "$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) DOCKER_VERSION_ALERT_SUPPRESS=1 # prevent docker version alert for exec 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 shift _exec "$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) DOCKER_VERSION_ALERT_SUPPRESS=1 # prevent version alert for exec 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 "$@" ;; acli) load_configuration shift _exec acli "$@" ;; wp) load_configuration shift if [[ "$1" == "" ]]; then _exec wp else _exec wp "$@" fi ;; composer) load_configuration shift _exec composer "$@" ;; ssh-add) echo-warning "${NC}Deprecated. Use ${yellow}fin ssh-key add${NC} instead." exit 1 ;; ssh-key) # no load_configuration shift check_docker_running if [[ "$1" == "" ]]; then show_help_ssh-key elif [[ "$1" == "new" ]]; then shift ssh_key_generate "$@" else ssh_key "$@" fi ;; docker|d) # no load_configuration shift check_winpty_found ${winpty} docker "$@" ;; docker-compose|dc) load_configuration shift docker-compose "$@" ;; docker-machine|dm) # no load_configuration shift 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 "$@" ;; share-v2) load_configuration shift case "$1" in start) shift cloudflared_start ;; stop) shift cloudflared_stop ;; status) shift cloudflared_print_status ;; url) shift cloudflared_print_url ;; logs) shift cloudflared_get_logs ;; *) shift show_help_share-v2 ;; esac ;; cleanup) # no load_configuration check_for_updates shift # Regular cleanup (Docksal stuff only) if [[ "$1" == "" ]]; then cleanup fi # Regular + hard cleanup (max cleanup to free up space - Docksal and non-Docksal dangling stuff) if [[ "$1" == "--hard" ]]; then cleanup cleanup_hard fi # Image cleanup wizard if [[ "$1" == "--images" ]]; then cleanup_images fi ;; -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 "$@" ;; yml|yaml) load_configuration config_yml "$@" ;; *) 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 "$@" ;; pull) # Todo: Deprecated Moving to Addon addon_file_path="$(addon_get_path $1)" if [[ -z "$addon_file_path" ]]; then echo-yellow "'fin pull' command has been removed out of core and into an addon. See https://github.com/docksal/addons/tree/master/pull for more details." exit 1 fi shift addon_execute "$addon_file_path" "$@" ;; *) 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