├── .gitignore ├── LICENSE.txt ├── README.md ├── circle.yml ├── lib └── rsync ├── src └── docker-osx-dev └── test ├── docker-osx-dev.bats ├── integration-test.sh ├── resources ├── docker-compose-base.yml ├── docker-compose-extends.yml ├── docker-compose-multiple-containers-with-volumes.yml ├── docker-compose-multiple-volumes.yml ├── docker-compose-named-volumes.yml ├── docker-compose-no-volumes.yml ├── docker-compose-non-mounted-volumes.yml ├── docker-compose-one-volume-access-modifier.yml ├── docker-compose-one-volume-double-quotes.yml ├── docker-compose-one-volume-single-quotes.yml ├── docker-compose-one-volume.yml ├── ignore-file-empty.txt ├── ignore-file-with-comments.txt ├── ignore-file-with-includes.txt ├── ignore-file-with-multiple-entries.txt └── ignore-file-with-one-entry.txt └── test_helper.bash /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | .AppleDouble 4 | .LSOverride 5 | 6 | # Vagrant 7 | .vagrant 8 | 9 | # Docker test project 10 | test-project 11 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Yevgeniy Brikman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # THIS PROJECT IS NO LONGER MAINTAINED 2 | 3 | As of March 4, 2018, this project is no longer actively maintained. The [Docker for Mac app](https://www.docker.com/docker-mac) 4 | has made significant improvements in terms of mounted volume performance and file watching, so `docker-osx-dev` is no longer necessary. 5 | 6 | # A productive development environment with Docker on OS X 7 | 8 | [Docker](https://www.docker.com/) and [Boot2Docker](http://boot2docker.io/) are 9 | awesome for running containers on OS X, but if you try to use them to do 10 | iterative development by mounting a source folder from OS X into your Docker 11 | container, you will run into two major problems: 12 | 13 | 1. Mounted volumes on VirtualBox use vboxsf, which is *extremely* slow, so 14 | compilation and startup times for code in mounted folders is 10-20x slower. 15 | 2. File watching is broken since vboxsf does not trigger the inotify file 16 | watching mechanism. The only workaround is to enable polling, which is *much* 17 | slower to pick up changes and eats up a lot of resources. 18 | 19 | I tried many different solutions (see [Alternatives](#alternatives)) that didn't 20 | work until I finally stumbled across one that does: 21 | [rsync](http://en.wikipedia.org/wiki/Rsync). With rsync, build and compilation 22 | performance in mounted folders is on par with native OS X performance and 23 | standard file watching mechanisms work properly too. However, setting it up 24 | correctly is a painful process that involves many steps, so to make life 25 | easier, I've packaged this process up in this **docker-osx-dev** project. 26 | 27 | For more info, check out the blog post [A productive development environment 28 | with Docker on OS X](http://www.ybrikman.com/writing/2015/05/19/docker-osx-dev/). 29 | 30 | # Status 31 | 32 | Beta. A number of developers are successfully using and contributing to 33 | docker-osx-dev. It still has some rough edges, but it works well, and makes 34 | the docker experience on OS X much better. Give it a try, share your feedback, 35 | and submit some pull requests! 36 | 37 | Note: this project is inherently a temporary workaround. I hope that in the 38 | future, someone will build a better alternative to vboxsf for mounting source 39 | code from OS X, and thereby make this entire project obsolete. Until that day 40 | comes, I will continue to use the docker-osx-dev scripts to keep myself productive. 41 | 42 | # Install 43 | 44 | Prerequisite: [HomeBrew](http://brew.sh/) must be installed. 45 | 46 | The `docker-osx-dev` script has an `install` command that can setup your entire 47 | Docker development environment on OS X, including installing Docker and 48 | Boot2Docker: 49 | 50 | ```sh 51 | curl -o /usr/local/bin/docker-osx-dev https://raw.githubusercontent.com/brikis98/docker-osx-dev/master/src/docker-osx-dev 52 | chmod +x /usr/local/bin/docker-osx-dev 53 | docker-osx-dev install 54 | ``` 55 | 56 | Four notes about the `install` command: 57 | 58 | 1. It is idempotent, so if you have some of the dependencies installed already, 59 | it will **not** overwrite them. 60 | 2. When the install completes, it prints out instructions for one `source` 61 | command you have to run to pick up important environment variables in your 62 | current shell, so make sure not to skip that step! 63 | 3. Once the install completes, you can use the `docker-osx-dev` script to sync 64 | files, as described in the next section. 65 | 4. It assumes the user you want to use for development can also run homebrew 66 | (eg. write to `/usr/local`). If it doesn't, you need to split the installation 67 | in 2 parts: one run as `admin` (the name of the user who can run homebrew), 68 | and one as yourself: 69 | ```sh 70 | su admin 71 | docker-osx-dev install --only-dependencies 72 | exit 73 | docker-osx-dev install --skip-dependencies 74 | ``` 75 | 76 | # Usage 77 | 78 | The `install` command will install, configure, and run Boot2Docker on your 79 | system, so the only thing left to do is to run the `docker-osx-dev` script and 80 | tell it what folders to sync. If you run it with no arguments, it will sync the 81 | current folder to the Boot2Docker VM: 82 | 83 | ``` 84 | > cd /foo/bar 85 | > docker-osx-dev 86 | [INFO] Performing initial sync of paths: /foo/bar 87 | [INFO] Watching: /foo/bar 88 | ``` 89 | 90 | Alternatively, you can use the `-s` flag to specify what folders to sync 91 | (run `docker-osx-dev -h` to see all supported options): 92 | 93 | ``` 94 | > docker-osx-dev -s /foo/bar 95 | [INFO] Performing initial sync of paths: /foo/bar 96 | [INFO] Watching: /foo/bar 97 | ``` 98 | 99 | Now, in a separate tab, you can run a Docker container and mount the current 100 | folder in it using the `-v` parameter. For example, here is how you can fire up 101 | the tiny [Alpine Linux image](https://registry.hub.docker.com/u/gliderlabs/alpine/) 102 | and get a Linux console in seconds: 103 | 104 | ``` 105 | > cd /foo/bar 106 | > docker run -v $(pwd):/src -it --rm gliderlabs/alpine:3.1 sh 107 | / # cd /src 108 | / # echo "I'm in a $(uname) container and my OS X files are being synced to $(pwd)!" 109 | I'm in a Linux container and my OS X files are being synced to /src! 110 | ``` 111 | 112 | As you make changes to the files in the `/foo/bar` folder on OS X, using the 113 | text editors, IDEs, and tools you're used to, they will be automatically 114 | synced to the `/src` folder in the Docker image. Moreover, file watchers should 115 | work normally in the Docker container for any framework that supports hot 116 | reload (e.g. Grunt, SBT, Jekyll) without any need for polling, so you should be 117 | able to follow a "make a change and refresh the page" development model. 118 | 119 | If you are using [Docker Compose](https://docs.docker.com/compose/), 120 | docker-osx-dev will automatically sync any folders marked as 121 | [volumes](https://docs.docker.com/compose/yml/#volumes) in `docker-compose.yml`. 122 | For example, let's say you had the following `docker-compose.yml` file: 123 | 124 | ```yml 125 | web: 126 | image: training/webapp 127 | volumes: 128 | - /foo:/src 129 | ports: 130 | - "5000:5000" 131 | db: 132 | image: postgres 133 | ``` 134 | 135 | First, run `docker-osx-dev`: 136 | 137 | ``` 138 | > docker-osx-dev 139 | [INFO] Using sync paths from Docker Compose file at docker-compose.yml 140 | [INFO] Performing initial sync of paths: /foo 141 | [INFO] Watching: /foo 142 | ``` 143 | 144 | Notice how it automatically found `/foo` in the `docker-compose.yml` file. 145 | Now you can start your Docker containers: 146 | 147 | ```sh 148 | docker-compose up 149 | ``` 150 | 151 | This will fire up a [Postgres 152 | database](https://registry.hub.docker.com/u/library/postgres/) and the [training 153 | webapp](https://registry.hub.docker.com/u/training/webapp/) (a simple "Hello, 154 | World" Python app), mount the `/foo` folder into `/src` in the webapp container, 155 | and expose port 5000. You can now test this webapp by going to: 156 | 157 | ``` 158 | http://dockerhost:5000 159 | ``` 160 | 161 | When you install docker-osx-dev, it adds an entry to your `/etc/hosts` file so 162 | that `http://dockerhost` works as a URL for testing your Docker containers. 163 | 164 | # docker-machine support 165 | 166 | `docker-machine` support is experimental. You can use it as the way it is used for 167 | `boot2docker`, but run `docker-machine env` before. So as an example, run as: 168 | 169 | ``` 170 | > docker-machine create --driver virtualbox 171 | > eval "$(docker-machine env )" 172 | > docker-osx-dev install 173 | > cd /foo/bar 174 | > docker-osx-dev 175 | [INFO] Performing initial sync of paths: /foo/bar 176 | [INFO] Watching: /foo/bar 177 | ``` 178 | 179 | In this case, `docker-osx-dev` will use the machine defined in the `DOCKER_MACHINE_NAME` env var, 180 | defined by `docker-machine env`. Alternatively, use the `--machine-name ` argument. 181 | 182 | Note: when running `docker-osx-dev` for `boot2docker`, please make sure the env var `DOCKER_MACHINE_NAME` 183 | is not defined. 184 | 185 | 186 | # How it works 187 | 188 | The `install command` installs all the software you need: 189 | 190 | 1. [Docker](https://www.docker.com/) 191 | 2. [Boot2Docker](http://boot2docker.io/) 192 | 3. [Docker Compose](https://docs.docker.com/compose/) 193 | 4. [VirtualBox](https://www.virtualbox.org/) 194 | 5. [fswatch](https://github.com/emcrisostomo/fswatch) 195 | 6. The `docker-osx-dev` script which you can use to start/stop file syncing 196 | 197 | The `install` command also: 198 | 199 | 1. Adds the Docker environment variables to your environment file (e.g. 200 | `~/.bash_profile`) so it is available at startup. 201 | 2. Adds an entry to `/etc/hosts` so that `http://dockerhost` works as a valid 202 | URL for your docker container for easy testing. 203 | 204 | Instead of using VirtualBox shared folders and vboxsf, docker-osx-dev keeps 205 | files in sync by using [fswatch](https://github.com/emcrisostomo/fswatch) to 206 | watch for changes and [rsync](http://en.wikipedia.org/wiki/Rsync) to quickly 207 | sync the files to the Boot2Docker VM. By default, the current source folder 208 | (i.e. the one you're in when you run `docker-osx-dev`) is synced. If you use 209 | `docker-compose`, docker-osx-dev will sync any folders marked as 210 | [volumes](https://docs.docker.com/compose/yml/#volumes). Run `docker-osx-dev -h` 211 | to see all the other options supported. 212 | 213 | # Limitations and known issues 214 | 215 | File syncing is currently one way only. That is, changes you make on OS X 216 | will be visible very quickly in the Docker container. However, changes in the 217 | Docker container will **not** be propagated back to OS X. This isn't a 218 | problem for most development scenarios, but time permitting, I'll be looking 219 | into using [Unison](http://www.cis.upenn.edu/~bcpierce/unison/) to support 220 | two-way sync. The biggest limitation at the moment is getting a build of 221 | Unison that will run on the Boot2Docker VM. 222 | 223 | # Contributing 224 | 225 | Contributions are very welcome via pull request. This project is in a very early 226 | alpha stage and it needs a lot of work. Take a look at the 227 | [issues](https://github.com/brikis98/docker-osx-dev/issues) for known bugs and 228 | enhancements, especially the ones marked with the 229 | [help wanted](https://github.com/brikis98/docker-osx-dev/labels/help%20wanted) 230 | tag. 231 | 232 | ## Running the code locally 233 | 234 | To run the local version of the code, just clone the repo and run your local 235 | copy of `docker-osx-dev`: 236 | 237 | ``` 238 | > git clone https://github.com/brikis98/docker-osx-dev.git 239 | > cd docker-osx-dev 240 | > ./src/docker-osx-dev 241 | ``` 242 | 243 | ## Running unit tests 244 | 245 | To run the unit tests, install [bats](https://github.com/sstephenson/bats) 246 | (`brew install bats`) and run the corresponding files in the `test` folder: 247 | 248 | ``` 249 | > ./test/docker-osx-dev.bats 250 | ✓ index_of doesn't find match in empty array 251 | ✓ index_of finds match in 1 item array 252 | ✓ index_of doesn't find match in 1 item array 253 | ✓ index_of finds match in 3 item array 254 | 255 | [...] 256 | 257 | 51 tests, 0 failures 258 | ``` 259 | 260 | ## Running integration tests 261 | 262 | I started to create integration tests for this project in 263 | `test/integration-test.sh`, but I hit a wall. The point of the integration test 264 | would be to run Boot2Docker in a VM, but most CI providers (e.g. TravisCI and 265 | CircleCI) already run your build in their own VM, so this would require running 266 | a VM-in-a-VM. As described in [#7](https://github.com/brikis98/docker-osx-dev/issues/7), 267 | I can't find any way to make this work. If anyone has any ideas, please take a 268 | look! 269 | 270 | # Alternatives 271 | 272 | Below are some of the other solutions I tried to make Docker productive on OS X 273 | (I even created a [StackOverflow Discussion](http://stackoverflow.com/questions/30090007/whats-the-right-way-to-setup-a-development-environment-on-os-x-with-docker) 274 | to find out what other people were doing.) With most of them, file syncing was 275 | still too slow to be usable, but they were useful to me to learn more about the 276 | Docker ecosystem, and perhaps they will be useful for you if docker-osx-dev 277 | doesn't work out: 278 | 279 | 1. [boot2docker-vagrant](https://github.com/blinkreaction/boot2docker-vagrant): 280 | Docker, Vagrant, and the ability to choose between NFS, Samba, rsync, and 281 | vboxsf for file syncing. A lot of the work in this project inspired 282 | docker-osx-dev. 283 | 2. [dinghy](https://github.com/codekitchen/dinghy): Docker + Vagrant + NFS. 284 | I found NFS was 2-3x slower than running builds locally, which was much 285 | faster than the 10-20x slowness of vboxsf, but still too slow to be usable. 286 | 3. [docker-unison](https://github.com/leighmcculloch/docker-unison): Docker + 287 | Unison. The [Unison File Synchronizer](http://www.cis.upenn.edu/~bcpierce/unison/) 288 | should be almost as fast as rsync, but I ran into [strange connection 289 | errors](https://github.com/leighmcculloch/docker-unison/issues/2) when I 290 | tried to use it with Docker. 291 | 4. [Polling in Jekyll](http://salizzar.net/2014/11/06/creating-a-github-jekyll-blog-using-docker/) 292 | and [Polling in SBT/Play](http://stackoverflow.com/a/26035919/483528). Some 293 | of the file syncing solutions, such as vboxsf and NFS, don't work correctly 294 | with file watchers that rely on inotify, so these are a couple examples of 295 | how to switch from file watching to polling. Unfortunately, this eats up a 296 | fair amount of resources and responds to file changes slower, especially as 297 | the project gets larger. 298 | 5. [Hodor](https://github.com/gansbrest/hodor). Uses the [Unison File 299 | Synchronizer](http://www.cis.upenn.edu/~bcpierce/unison/) to sync files. I 300 | have not had a chance to try this project out yet. 301 | 6. [docker-machine-nfs](https://github.com/adlogix/docker-machine-nfs): Activates NFS 302 | for an existing machine. 303 | 304 | # License 305 | 306 | This code is released under the MIT License. See LICENSE.txt. 307 | 308 | # Changelog 309 | 310 | * 06/05/15: merged the `setup.sh` and `docker-osx-dev` scripts together since 311 | they share a lot of the same code and bash scripts don't have any easy ways 312 | to define modules, download dependencies, etc. 313 | * 05/25/15: Second version released. Removes Vagrant dependency and uses just 314 | rsync + Boot2Docker. If you had installed the first version, you should 315 | delete your `Vagrantfile`, delete the old version of 316 | `/usr/local/bin/docker-osx-dev`, and re-run the `setup.sh` script. 317 | * 05/19/15: Initial version released. Uses Vagrant + rsync + Boot2Docker. 318 | 319 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | environment: 3 | XCODE_WORKSPACE: "$HOME/docker-osx-dev" # Dummy value since this project doesn't use XCode or iOS 4 | XCODE_SCHEME: "$HOME/docker-osx-dev" # Dummy value since this project doesn't use XCode or iOS 5 | dependencies: 6 | post: 7 | - brew install bats coreutils 8 | test: 9 | override: 10 | - ./test/docker-osx-dev.bats 11 | # Disabled until we can solve https://github.com/brikis98/docker-osx-dev/issues/7 12 | # - ./test/integration-test.sh 13 | -------------------------------------------------------------------------------- /lib/rsync: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brikis98/docker-osx-dev/8d16d03ceb2866ed1284ea57dc94d0003df6ed8c/lib/rsync -------------------------------------------------------------------------------- /src/docker-osx-dev: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # A script for running a productive development environment with Docker 4 | # on OS X. See https://github.com/brikis98/docker-osx-dev for more info. 5 | 6 | set -e 7 | 8 | # Console colors 9 | readonly COLOR_DEBUG='\033[1;36m' 10 | readonly COLOR_INFO='\033[0;32m' 11 | readonly COLOR_WARN='\033[1;33m' 12 | readonly COLOR_ERROR='\033[0;31m' 13 | readonly COLOR_INSTRUCTIONS='\033[0;37m' 14 | readonly COLOR_END='\033[0m' 15 | 16 | # Log levels 17 | readonly LOG_LEVEL_DEBUG="DEBUG" 18 | readonly LOG_LEVEL_INFO="INFO" 19 | readonly LOG_LEVEL_WARN="WARN" 20 | readonly LOG_LEVEL_ERROR="ERROR" 21 | readonly LOG_LEVEL_INSTRUCTIONS="INSTRUCTIONS" 22 | readonly LOG_LEVELS="$LOG_LEVEL_DEBUG $LOG_LEVEL_INFO $LOG_LEVEL_WARN $LOG_LEVEL_ERROR $LOG_LEVEL_INSTRUCTIONS" 23 | readonly DEFAULT_LOG_LEVEL="$LOG_LEVEL_INFO" 24 | 25 | # Environment variable file constants 26 | readonly BASH_PROFILE="$HOME/.bash_profile" 27 | readonly BASH_RC="$HOME/.bashrc" 28 | readonly ZSH_RC="$HOME/.zshrc" 29 | readonly ENV_FILE_COMMENT="\n# docker-osx-dev\n" 30 | 31 | # Script constants 32 | readonly HOSTS_FILE="/etc/hosts" 33 | readonly SYNC_COMMAND="sync" 34 | readonly WATCH_ONLY_COMMAND="watch-only" 35 | readonly SYNC_ONLY_COMMAND="sync-only" 36 | readonly INSTALL_COMMAND="install" 37 | readonly TEST_COMMAND="test_mode" 38 | readonly DEFAULT_COMMAND="$SYNC_COMMAND" 39 | readonly BIN_DIR="/usr/local/bin" 40 | 41 | # docker host constants 42 | readonly BOOT2DOCKER_USER="docker" 43 | 44 | # docker-compose constants 45 | readonly DEFAULT_COMPOSE_FILE="docker-compose.yml" 46 | 47 | # Sync and watch constants 48 | readonly DEFAULT_PATHS_TO_SYNC="." 49 | readonly DEFAULT_EXCLUDES=".git" 50 | readonly DEFAULT_IGNORE_FILE=".dockerignore" 51 | readonly RSYNC_FLAGS="--archive --log-format 'Syncing %n: %i' --delete --omit-dir-times --inplace --whole-file -l" 52 | 53 | # docker-osx-dev repo constants 54 | readonly RSYNC_BINARY_URL="https://github.com/brikis98/docker-osx-dev/blob/master/lib/rsync?raw=true" 55 | 56 | # Global variables. The should only ever be set by the corresponding 57 | # configure_XXX functions. 58 | PATHS_TO_SYNC="" 59 | EXCLUDES="" 60 | INCLUDES="" 61 | CURRENT_LOG_LEVEL="$DEFAULT_LOG_LEVEL" 62 | DOCKER_HOST_USER="" 63 | DOCKER_HOST_SSH_URL="" 64 | DOCKER_HOST_SSH_KEY="" 65 | DOCKER_HOST_SSH_COMMAND="" 66 | 67 | 68 | ################################################################################ 69 | # Utility functions 70 | ################################################################################ 71 | 72 | # 73 | # Dumps a 'stack trace' for failed assertions. 74 | # 75 | function backtrace { 76 | local readonly max_trace=20 77 | local frame=0 78 | while test $frame -lt $max_trace ; do 79 | frame=$(( $frame + 1 )) 80 | local bt_file=${BASH_SOURCE[$frame]} 81 | local bt_function=${FUNCNAME[$frame]} 82 | local bt_line=${BASH_LINENO[$frame-1]} # called 'from' this line 83 | if test -n "${bt_file}${bt_function}" ; then 84 | log_error " at ${bt_file}:${bt_line} ${bt_function}()" 85 | fi 86 | done 87 | } 88 | 89 | # 90 | # Usage: assert_non_empty VAR 91 | # 92 | # Asserts that VAR is not empty and exits with an error code if it is. 93 | # 94 | function assert_non_empty { 95 | local readonly var="$1" 96 | 97 | if test -z "$var" ; then 98 | log_error "internal error: unexpected empty-string argument" 99 | backtrace 100 | exit 1 101 | fi 102 | } 103 | 104 | # 105 | # Usage: assert_mutually_exclusive ERROR_MESSAGE VAR1 VAR2 ... VARn 106 | # 107 | # Asserts that at most one of VAR1, VAR2, ..., VARn is not empty 108 | # 109 | function assert_mutually_exclusive { 110 | local readonly error_message=$1 111 | shift 112 | local found 113 | while [[ $# > 0 ]]; do 114 | local value=$1 115 | if test -n "$value" ; then 116 | if test -n "$found" ; then 117 | log_error "$error_message" 118 | instructions 119 | exit 1 120 | else 121 | found=true 122 | fi 123 | fi 124 | shift 125 | done 126 | } 127 | 128 | # 129 | # Usage: index_of VALUE ARRAY 130 | # 131 | # Returns the first index where VALUE appears in ARRAY. If ARRAY does not 132 | # contain VALUE, returns -1. 133 | # 134 | # Examples: 135 | # 136 | # arr=("abc" "foo" "def") 137 | # index_of foo "${arr[@]}" 138 | # Returns: 1 139 | # 140 | # arr=("abc" "def") 141 | # index_of foo "${arr[@]}" 142 | # Returns -1 143 | # 144 | # index_of foo "abc" "def" "foo" 145 | # Returns 2 146 | # 147 | function index_of { 148 | local readonly value="$1" 149 | shift 150 | local readonly array=("$@") 151 | local i=0 152 | 153 | for (( i = 0; i < ${#array[@]}; i++ )); do 154 | if [ "${array[$i]}" = "${value}" ]; then 155 | echo $i 156 | return 157 | fi 158 | done 159 | 160 | echo -1 161 | } 162 | 163 | # 164 | # Usage: join SEPARATOR ARRAY 165 | # 166 | # Joins the elements of ARRAY with the SEPARATOR character between them. 167 | # 168 | # Examples: 169 | # 170 | # join ", " ("A" "B" "C") 171 | # Returns: "A, B, C" 172 | # 173 | function join { 174 | local readonly separator="$1" 175 | shift 176 | local readonly values=("$@") 177 | 178 | printf "%s$separator" "${values[@]}" | sed "s/$separator$//" 179 | } 180 | 181 | ################################################################################ 182 | # Logging 183 | ################################################################################ 184 | 185 | # 186 | # Returns the current timestamp formatted for logging 187 | # 188 | function format_timestamp { 189 | date +"%Y-%m-%d %H:%M:%S" 190 | } 191 | 192 | # Helper function to log an INFO message. See the log function for details. 193 | function log_info { 194 | log "$COLOR_INFO" "$COLOR_END" "$(format_timestamp)" "$LOG_LEVEL_INFO" "$@" 195 | } 196 | 197 | # Helper function to log a WARN message. See the log function for details. 198 | function log_warn { 199 | log "$COLOR_WARN" "$COLOR_END" "$(format_timestamp)" "$LOG_LEVEL_WARN" "$@" 200 | } 201 | 202 | # Helper function to log a DEBUG message. See the log function for details. 203 | function log_debug { 204 | log "$COLOR_DEBUG" "$COLOR_END" "$(format_timestamp)" "$LOG_LEVEL_DEBUG" "$@" 205 | } 206 | 207 | # Helper function to log an ERROR message. See the log function for details. 208 | function log_error { 209 | log "$COLOR_ERROR" "$COLOR_END" "$(format_timestamp)" "$LOG_LEVEL_ERROR" "$@" 210 | } 211 | 212 | # Helper function to log an INSTRUCTIONS message. See the log function for details. 213 | function log_instructions { 214 | log "$COLOR_INSTRUCTIONS" "$COLOR_END" "$(format_timestamp)" "$LOG_LEVEL_INSTRUCTIONS" "$@" 215 | } 216 | 217 | # 218 | # Usage: log COLOR COLOR_END TIMESTAMP LEVEL [MESSAGE ...] 219 | # 220 | # Logs MESSAGE, at time TIMESTAMP, surrounded by COLOR and COLOR_END, to stdout 221 | # if the log level is at least LEVEL. If no MESSAGE is specified, reads from 222 | # stdin. The log level is determined by the DOCKER_OSX_DEV_LOG_LEVEL environment 223 | # variable. 224 | # 225 | # Examples: 226 | # 227 | # log "\033[0;32m" "\033[0m" "2015-06-03 15:30:33" "INFO" "Hello, World" 228 | # Prints: "\033[0;32m2015-06-03 15:30:33 [INFO] Hello, World\033[0m" to stdout. 229 | # 230 | # echo "Hello, World" | log "\033[0;32m" "\033[0m" "2015-06-03 15:30:33" "ERROR" 231 | # Prints: "\033[0;32m2015-06-03 15:30:33 [ERROR] Hello, World\033[0m" to stdout. 232 | # 233 | function log { 234 | if [[ "$#" -gt 4 ]]; then 235 | do_log "$@" 236 | elif [[ "$#" -eq 4 ]]; then 237 | local message="" 238 | while read message; do 239 | do_log "$1" "$2" "$3" "$4" "$message" 240 | done 241 | else 242 | echo "Internal error: invalid number of arguments passed to log function: $@" 243 | exit 1 244 | fi 245 | } 246 | 247 | # 248 | # Usage: do_log COLOR COLOR_END TIMESTAMP LEVEL MESSAGE ... 249 | # 250 | # Logs MESSAGE, at time TIMESTAMP, surrounded by COLOR and COLOR_END, to stdout 251 | # if the log level is at least LEVEL. The log level is determined by the 252 | # DOCKER_OSX_DEV_LOG_LEVEL environment variable. 253 | # 254 | # Examples: 255 | # 256 | # do_log "\033[0;32m" "\033[0m" "INFO" "Hello, World" 257 | # Prints: "\033[0;32m[INFO] Hello, World\033[0m" to stdout. 258 | # 259 | function do_log { 260 | local readonly color="$1" 261 | shift 262 | local readonly color_end="$1" 263 | shift 264 | local readonly timestamp="$1" 265 | shift 266 | local readonly log_level="$1" 267 | shift 268 | local readonly message="$@" 269 | 270 | local readonly log_level_index=$(index_of "$log_level" $LOG_LEVELS) 271 | local readonly current_log_level_index=$(index_of "$CURRENT_LOG_LEVEL" $LOG_LEVELS) 272 | 273 | if [[ "$log_level_index" -ge "$current_log_level_index" ]]; then 274 | echo -e "${color}${timestamp} [${log_level}] ${message}${color_end}" 275 | fi 276 | } 277 | 278 | # 279 | # Usage: assert_valid_log_level LEVEL 280 | # 281 | # Asserts that LEVEL is a valid log level--that is, it's one of the values in 282 | # LOG_LEVELS. 283 | # 284 | function assert_valid_log_level { 285 | local readonly level="$1" 286 | local readonly index=$(index_of "$level" $LOG_LEVELS) 287 | 288 | if [[ "$index" -lt 0 ]]; then 289 | echo "Invalid log level specified: $level" 290 | instructions 291 | exit 1 292 | fi 293 | } 294 | 295 | # 296 | # Usage: configure_log_level LEVEL 297 | # 298 | # Set the logging level to LEVEL. LEVEL must be one of the values in LOG_LEVELS. 299 | # 300 | function configure_log_level { 301 | local readonly level="$1" 302 | assert_valid_log_level "$level" 303 | CURRENT_LOG_LEVEL="$level" 304 | } 305 | 306 | ################################################################################ 307 | # Boot2Docker manipulation 308 | ################################################################################ 309 | 310 | # 311 | # Configures the Boot2Docker SSH key by looking into the Boot2Docker config 312 | # 313 | function configure_boot2docker { 314 | test -n "$DOCKER_HOST_NAME" || DOCKER_HOST_NAME="dockerhost" 315 | DOCKER_HOST_SSH_KEY=$(boot2docker cfg | grep "^SSHKey = " | sed -e 's/^SSHKey = "\(.*\)"/\1/') 316 | DOCKER_HOST_USER="$BOOT2DOCKER_USER" 317 | DOCKER_HOST_SSH_URL="$BOOT2DOCKER_USER@$DOCKER_HOST_NAME" 318 | DOCKER_HOST_SSH_COMMAND="boot2docker ssh" 319 | } 320 | 321 | # 322 | # Usage: find_vboxsf_mounted_folders 323 | # 324 | # Returns mounted volumes with vboxsf-type in boot2docker instance. 325 | # 326 | function find_vboxsf_mounted_folders { 327 | $DOCKER_HOST_SSH_COMMAND mount | grep 'type vboxsf' | awk '{print $3}' 328 | } 329 | 330 | # 331 | # Usage: umount_vboxsf_mounted_folder SHARED_FOLDERS 332 | # 333 | # Remove the VirtualBox shared folder from the boot2docker VM. 334 | # SHARED_FOLDERS should be the output of the 335 | # find_vboxsf_mounted_folders function. 336 | # 337 | function umount_vboxsf_mounted_folder { 338 | local readonly vbox_shared_folders="$1" 339 | local vbox_shared_folder='' 340 | while read -r vbox_shared_folder; do 341 | log_info "Removing shared folder: $vbox_shared_folder" 342 | $DOCKER_HOST_SSH_COMMAND sudo umount "$vbox_shared_folder" 343 | done <<< "$vbox_shared_folders" 344 | } 345 | 346 | # 347 | # Usage: check_for_shared_folders REMOVE_SHARED_FOLDERS 348 | # 349 | # Checks if the docker host has any VirtualBox shared folders. If so, prompt 350 | # the user if they would like to remove them, as they will void any benefits 351 | # from using rsync. If AUTOREMOVE is true, the folders will be removed without 352 | # prompting the user. 353 | # 354 | function check_for_shared_folders { 355 | local readonly remove_shared_folders="$1" 356 | local readonly vbox_shared_folders=$(find_vboxsf_mounted_folders) 357 | 358 | if [[ ! -z "$vbox_shared_folders" ]]; then 359 | log_error "Found VirtualBox shared folders on your Boot2Docker VM. These may void any performance benefits from using docker-osx-dev:\n$vbox_shared_folders" 360 | 361 | if [[ "$remove_shared_folders" = true ]]; then 362 | log_info "Autoremoving shared folders." 363 | umount_vboxsf_mounted_folder "$vbox_shared_folders" 364 | else 365 | log_instructions "Would you like this script to remove them?" 366 | local choice="" 367 | select choice in "yes" "no"; do 368 | case $REPLY in 369 | y|Y|yes|Yes ) 370 | umount_vboxsf_mounted_folder "$vbox_shared_folders" 371 | break 372 | ;; 373 | n|N|no|No ) 374 | log_instructions "Please remove the VirtualBox shares yourself and re-run this script. Exiting." 375 | exit 1 376 | ;; 377 | esac 378 | done 379 | fi 380 | fi 381 | } 382 | 383 | # 384 | # Returns true iff the Boot2Docker VM is initialized 385 | # 386 | function is_boot2docker_initialized { 387 | boot2docker status >/dev/null 2>&1 388 | } 389 | 390 | # 391 | # Returns true iff the Boot2Docker VM is running 392 | # 393 | function is_boot2docker_running { 394 | local readonly status=$(boot2docker status 2>&1) 395 | test "$status" = "running" 396 | } 397 | 398 | # 399 | # Usage: init_boot2docker REMOVE_SHARED_FOLDERS 400 | # 401 | # Initializes and starts up the Boot2Docker VM. 402 | # 403 | function init_boot2docker { 404 | local readonly remove_shared_folders=$1 405 | 406 | if ! is_boot2docker_initialized; then 407 | log_info "Initializing Boot2Docker VM" 408 | boot2docker init 409 | fi 410 | 411 | if ! is_boot2docker_running; then 412 | log_info "Starting Boot2Docker VM" 413 | boot2docker start --vbox-share=disable 414 | fi 415 | 416 | configure_boot2docker 417 | check_for_shared_folders "$remove_shared_folders" 418 | } 419 | 420 | ################################################################################ 421 | # docker-machine manipulation 422 | ################################################################################ 423 | 424 | 425 | # 426 | # Inspect specific configuration about the docker-machine vm 427 | # 428 | function inspect_docker_machine { 429 | local result=$(docker-machine inspect --format="$*" "$DOCKER_MACHINE_NAME" 2>&1) 430 | if test "${result}" != ""; then 431 | echo ${result} 432 | else 433 | return -1 434 | fi 435 | } 436 | 437 | # 438 | # Configures variables based on the output of `docker-machine inspect $DOCKER_MACHINE_NAME` 439 | # 440 | function configure_docker_machine { 441 | DOCKER_HOST_NAME="$DOCKER_MACHINE_NAME" 442 | # Support < 0.4.1 and >= 0.5.1 443 | DOCKER_HOST_USER=$(inspect_docker_machine "{{.Driver.SSHUser}}" || inspect_docker_machine "{{.Driver.Driver.SSHUser}}") 444 | DOCKER_HOST_IP=$(docker-machine ip "$DOCKER_MACHINE_NAME" || inspect_docker_machine "{{.Driver.IPAddress}}" || inspect_docker_machine "{{.Driver.Driver.IPAddress}}") 445 | # Support both version 0.4.1 (and earlier) and 0.5.0 (and later) 446 | DOCKER_MACHINE_STORE_PATH=$(inspect_docker_machine "{{.StorePath}}" || inspect_docker_machine "{{.HostOptions.AuthOptions.StorePath}}") 447 | DOCKER_MACHINE_DRIVER_NAME=$(inspect_docker_machine "{{.DriverName}}") 448 | 449 | DOCKER_HOST_SSH_URL="$DOCKER_HOST_USER@$DOCKER_HOST_IP" 450 | # 0.5.0 returns StorePath as a root directory /machine 451 | DOCKER_HOST_SSH_KEY="$DOCKER_MACHINE_STORE_PATH/machines/$DOCKER_MACHINE_NAME/id_rsa" 452 | 453 | if [[ ! -f $DOCKER_HOST_SSH_KEY ]]; then 454 | # 0.4.1 returns StorePath as /machines/dev 455 | DOCKER_HOST_SSH_KEY="$DOCKER_MACHINE_STORE_PATH/id_rsa" 456 | fi 457 | 458 | if [[ $CURRENT_LOG_LEVEL == "DEBUG" ]]; then 459 | DOCKER_HOST_SSH_COMMAND="docker-machine -D ssh $DOCKER_MACHINE_NAME" 460 | else 461 | DOCKER_HOST_SSH_COMMAND="docker-machine ssh $DOCKER_MACHINE_NAME" 462 | fi 463 | } 464 | 465 | # 466 | # Usage: init_docker_machine REMOVE_SHARED_FOLDERS 467 | # 468 | # Initializes and starts up the docker-machine VM. 469 | # 470 | function init_docker_machine { 471 | local readonly remove_shared_folders=$1 472 | 473 | if ! is_docker_machine_running; then 474 | log_info "Initializing docker machine $DOCKER_MACHINE_NAME" 475 | docker-machine start "$DOCKER_MACHINE_NAME" 476 | fi 477 | eval "$(docker-machine env --shell bash $DOCKER_MACHINE_NAME)" 478 | configure_docker_machine 479 | if [[ $DOCKER_MACHINE_DRIVER_NAME == "virtualbox" ]]; then 480 | check_for_shared_folders "$remove_shared_folders" 481 | fi 482 | } 483 | 484 | # 485 | # Returns true iff the docker-machine VM is running 486 | # 487 | function is_docker_machine_running { 488 | log_info "Testing if docker machine is running" 489 | local readonly status=$(docker-machine status $DOCKER_MACHINE_NAME 2>&1) 490 | test "$status" = "Running" 491 | } 492 | 493 | ################################################################################ 494 | # Generic docker host manipulation 495 | ################################################################################ 496 | 497 | # 498 | # Usage: init_docker_host REMOVE_SHARED_FOLDERS 499 | # 500 | # Initializes a docker host 501 | # Initializes boot2docker, unless DOCKER_MACHINE_NAME is defined 502 | # 503 | function init_docker_host { 504 | local readonly remove_shared_folders=$1 505 | if [[ -n "$DOCKER_MACHINE_NAME" ]]; then 506 | init_docker_machine "$remove_shared_folders" 507 | else 508 | init_boot2docker "$remove_shared_folders" 509 | fi 510 | } 511 | 512 | # 513 | # Installs rsync on the Boot2Docker VM, unless it's already installed. 514 | # If the main repository is down, rsync will be installed from a mirror 515 | # If the mirror is down, download rsync binary and place in Boot2Docker VM 516 | # 517 | function install_rsync_on_docker_host { 518 | log_info "Installing rsync in the Docker Host image" 519 | 520 | if ! $DOCKER_HOST_SSH_COMMAND "if ! type rsync > /dev/null 2>&1; then tce-load -wi rsync; fi"; then 521 | 522 | # For some reason, the tce-load command often exits with an error code, even 523 | # if rsync installed successfully. Therefore, re-run just the type command 524 | # to see if it actually worked 525 | 526 | if ! $DOCKER_HOST_SSH_COMMAND "type rsync > /dev/null 2>&1" ; then 527 | log_info "Failed to install rsync using tce-load, falling back to install rsync from pre-built binary in docker-osx-dev GitHub repo" 528 | $DOCKER_HOST_SSH_COMMAND "sudo mkdir -p $BIN_DIR && sudo wget -O $BIN_DIR/rsync $RSYNC_BINARY_URL && sudo chmod +x $BIN_DIR/rsync" 529 | fi 530 | fi 531 | } 532 | 533 | ################################################################################ 534 | # Environment setup 535 | ################################################################################ 536 | 537 | # 538 | # Usage: env_is_defined VAR 539 | # 540 | # Checks if a new SHELL has VAR defined in its environment. 541 | # Returns 0 when VAR is defined for new shells, 1 otherwise. 542 | # 543 | function env_is_defined { 544 | local readonly var="$1" 545 | assert_non_empty "$var" 546 | 547 | local readonly setting=$(env | grep "^${var}=") 548 | test -n "$setting" 549 | } 550 | 551 | # 552 | # Usage: get_env_file 553 | # 554 | # Tries to find and return the proper environment file for the current user. 555 | # 556 | # Examples: 557 | # 558 | # get_env_file 559 | # Returns: ~/.bash_profile 560 | # 561 | function get_env_file { 562 | if [[ -f "$BASH_RC" ]]; then 563 | echo "$BASH_RC" 564 | elif [[ -f "$ZSH_RC" ]]; then 565 | echo "$ZSH_RC" 566 | else 567 | echo "$BASH_PROFILE" 568 | fi 569 | } 570 | 571 | # 572 | # Adds environment variables necessary for running Boot2Docker 573 | # 574 | function add_environment_variables { 575 | if [ -z "$DOCKER_MACHINE_NAME" ]; then 576 | local readonly env_file=$(get_env_file) 577 | local readonly boot2docker_exports=$(boot2docker shellinit 2>/dev/null) 578 | local readonly exports_to_add_to_env_file=$(determine_boot2docker_exports_for_env_file "$boot2docker_exports") 579 | 580 | if [[ ! -z "$exports_to_add_to_env_file" ]]; then 581 | log_info "Adding new environment variables to $env_file: $exports_to_add_to_env_file" 582 | echo -e "$exports_to_add_to_env_file" >> "$env_file" 583 | log_instructions "To pick up important new environment variables in the current shell, run:\n\tsource $env_file" 584 | else 585 | log_warn "All Boot2Docker environment variables already defined, will not overwrite" 586 | fi 587 | fi 588 | } 589 | 590 | # 591 | # Usage: determine_boot2docker_exports_for_env_file BOOT2DOCKER_SHELLINIT_EXPORTS 592 | # 593 | # Parses BOOT2DOCKER_SHELLINIT_EXPORTS, which should be the output of the 594 | # boot2docker shelinit command, and returns a string of the exports that are 595 | # not already in the current environment. 596 | # 597 | function determine_boot2docker_exports_for_env_file { 598 | local readonly boot2docker_exports="$1" 599 | 600 | local exports_to_add_to_env_file=() 601 | local export_line="" 602 | 603 | while read -r export_line; do 604 | if [[ ! -z "$export_line" ]]; then 605 | local readonly var_name=$(echo "$export_line" | sed -ne 's/export \(.*\)=.*/\1/p') 606 | 607 | if [[ -z "$var_name" ]]; then 608 | log_error "Unexpected entry from boot2docker shellinit: $export_line" 609 | exit 1 610 | elif ! env_is_defined "$var_name"; then 611 | exports_to_add_to_env_file+=("$export_line") 612 | fi 613 | fi 614 | done <<< "$boot2docker_exports" 615 | 616 | if [[ "${#exports_to_add_to_env_file[@]}" -gt 0 ]]; then 617 | local exports_as_string=$(join "\n" "${exports_to_add_to_env_file[@]}") 618 | echo -e "$ENV_FILE_COMMENT$exports_as_string" 619 | else 620 | echo "" 621 | fi 622 | } 623 | 624 | # 625 | # Adds Docker entries to /etc/hosts 626 | # 627 | function add_docker_host { 628 | if grep -q "^[^#]*$DOCKER_HOST_NAME" "$HOSTS_FILE" ; then 629 | log_warn "$HOSTS_FILE already contains $DOCKER_HOST_NAME, will not overwrite" 630 | else 631 | DOCKER_HOST_IP=${DOCKER_HOST_IP-$(boot2docker ip)} 632 | local readonly host_entry="\n$DOCKER_HOST_IP $DOCKER_HOST_NAME" 633 | 634 | log_info "Adding $DOCKER_HOST_NAME entry to $HOSTS_FILE so you can use http://$DOCKER_HOST_NAME URLs for testing" 635 | log_instructions "Modifying $HOSTS_FILE requires sudo privileges, please enter your password." 636 | sudo -k sh -c "echo \"$host_entry\" >> $HOSTS_FILE" 637 | fi 638 | } 639 | 640 | ################################################################################ 641 | # Installation 642 | ################################################################################ 643 | 644 | # 645 | # Checks that this script can be run on the current machine and exits with an 646 | # error code if any of the requirements are missing. 647 | # 648 | function check_prerequisites { 649 | local readonly os=$(uname) 650 | 651 | if [[ ! "$os" = "Darwin" ]]; then 652 | log_error "This script should only be run on OS X" 653 | exit 1 654 | fi 655 | } 656 | 657 | # 658 | # Update Brew formulae and HomeBrew 659 | # 660 | # Non-breaking. Falls through with warning if fails. 661 | # 662 | function brew_update { 663 | log_info "Updating HomeBrew" 664 | if ! type brew > /dev/null 2>&1 ; then 665 | log_warn "HomeBrew not found, will not attempt to update it" 666 | else 667 | brew update 668 | fi 669 | } 670 | 671 | # 672 | # Usage: brew_install PACKAGE_NAME READABLE_NAME EXISTENCE_TEST USE_CASK 673 | # 674 | # Checks if PACKAGE_NAME is already installed by using brew as well as by 675 | # running `eval EXISTENCE_TEST` and if it can't find it, uses brew to 676 | # install PACKAGE_NAME. If USE_CASK is set to true, uses brew cask 677 | # instead. 678 | # 679 | # Examples: 680 | # 681 | # brew_install virtualbox VirtualBox 'type vboxwebsrv' true 682 | # Result: checks if brew cask already has virtualbox installed or 683 | # `type vboxwebsrv` succeeds, and if not, uses brew cask to install it. 684 | # 685 | function brew_install { 686 | local readonly package_name="$1" 687 | local readonly readable_name="$2" 688 | local readonly existence_test="$3" 689 | local readonly use_cask="$4" 690 | 691 | if ! eval "$existence_test" > /dev/null 2>&1 ; then 692 | check_homebrew $readable_name 693 | 694 | local brew_command="brew" 695 | if [[ "$use_cask" = true ]]; then 696 | brew_command="brew cask" 697 | fi 698 | 699 | if eval "$brew_command list $package_name" > /dev/null 2>&1 ; then 700 | log_warn "$readable_name is already installed by HomeBrew, skipping" 701 | else 702 | log_info "Installing $readable_name" 703 | eval "$brew_command install $package_name" 704 | 705 | # It seems that brew install can fail to install a dependency, but exit 706 | # without an error code, so the set -e flag does not help us. Example: 707 | # https://github.com/brikis98/docker-osx-dev/issues/124 708 | # Therefore, explicitly check that the dependency was installed before 709 | # proceeding. 710 | if ! eval "$existence_test" > /dev/null 2>&1; then 711 | log_error "Failed to install dependency $readable_name. Check the log output above for reasons." 712 | exit 1 713 | fi 714 | fi 715 | else 716 | log_warn "Test \"$existence_test\" passed, assuming $readable_name is already installed and skipping" 717 | fi 718 | } 719 | 720 | # 721 | # Checks if HomeBrew is installed. 722 | # Called only in case of a missing dependency. 723 | # 724 | function check_homebrew { 725 | local readonly missing_package="$1" 726 | if ! type brew > /dev/null 2>&1 ; then 727 | log_error "HomeBrew is required to install $missing_package, but it's not installed. Aborting." 728 | exit 1 729 | fi 730 | } 731 | 732 | # 733 | # Installs all the dependencies for docker-osx-dev. 734 | # 735 | function install_dependencies { 736 | brew_update 737 | 738 | log_info "Setting up Cask" 739 | if ! brew tap caskroom/cask; then 740 | log_error "Failed to set up Cask. Check the log output above for reasons." 741 | exit 1 742 | fi 743 | 744 | brew_install "caskroom/cask/brew-cask" "Cask" "brew cask list" false 745 | brew_install "boot2docker" "Boot2Docker" "type boot2docker" false 746 | brew_install "docker-compose" "Docker Compose" "type docker-compose" false 747 | brew_install "docker-machine" "Docker Machine" "type docker-machine" false 748 | brew_install "fswatch" "fswatch" "type fswatch" false 749 | brew_install "coreutils" "GNU core utilities" "type greadlink" false 750 | } 751 | 752 | # 753 | # Prints instructions on what the user should do next 754 | # 755 | function print_next_steps { 756 | log_info "docker-osx-dev setup has completed successfully." 757 | log_instructions "You can now start file syncing using the docker-osx-dev" \ 758 | "script and run Docker containers using docker run. Example:\n\t" \ 759 | "> docker-osx-dev\n\t" \ 760 | '> docker run -v $(pwd):/src some-docker-container' 761 | } 762 | 763 | ################################################################################ 764 | # File syncing 765 | ################################################################################ 766 | 767 | # 768 | # Usage: find_path_to_sync_parent PATH 769 | # 770 | # Finds the parent folder of PATH from the PATHS_TO_SYNC global variable. When 771 | # using rsync, we want to sync the exact folders the user specified when 772 | # running the docker-osx-dev script. However, when we we use fswatch, it gives 773 | # us the path of files that changed, which may be deeply nested inside one of 774 | # the folders we're supposed to keep in sync. Therefore, this function lets us 775 | # transform one of these nested paths back to one of the top level rsync paths. 776 | # 777 | function find_path_to_sync_parent { 778 | local readonly path="$1" 779 | local readonly normalized_path=$(greadlink -m $(eval echo '$path')) 780 | local readonly paths_to_sync=($PATHS_TO_SYNC) 781 | 782 | local path_to_sync="" 783 | for path_to_sync in "${paths_to_sync[@]}"; do 784 | if [[ "$normalized_path" == $path_to_sync || "$normalized_path" == $path_to_sync\/* ]]; then 785 | echo "$path_to_sync" 786 | return 787 | fi 788 | done 789 | } 790 | 791 | # 792 | # Usage: rsync PATH 793 | # 794 | # Uses rsync to sync PATH to the same PATH on the Boot2Docker VM. 795 | # 796 | # Examples: 797 | # 798 | # rsync /foo 799 | # Result: the contents of /foo are rsync'ed to /foo on the Boot2Docker VM 800 | # 801 | function do_rsync { 802 | local readonly path="$1" 803 | local readonly path_to_sync=$(find_path_to_sync_parent "$path") 804 | 805 | if [[ -z "$path_to_sync" ]]; then 806 | log_error "Internal error: can't sync '$path' because it doesn't seem to be part of any paths configured for syncing: $PATHS_TO_SYNC" 807 | else 808 | local readonly parent_folder=$(dirname "$path_to_sync") 809 | 810 | local excludes=() 811 | read -a excludes <<< "$EXCLUDES" 812 | local readonly exclude_flags="${excludes[@]/#/--exclude }" 813 | 814 | local includes=() 815 | read -a includes <<< "$INCLUDES" 816 | local readonly include_flags="${includes[@]/#/--include }" 817 | 818 | local readonly rsh_flag="--rsh=\"ssh -i $DOCKER_HOST_SSH_KEY -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\"" 819 | 820 | local readonly rsync_cmd="rsync $RSYNC_FLAGS $include_flags $exclude_flags $rsh_flag $path_to_sync $DOCKER_HOST_SSH_URL:$parent_folder 2>&1 | grep -v \"^Warning: Permanently added\"" 821 | log_debug "$rsync_cmd" 822 | 823 | eval "$rsync_cmd" 2>&1 | log_info 824 | fi 825 | } 826 | 827 | # 828 | # Usage: do_sync [PATHS ...] 829 | # 830 | # Uses rsync to sync PATHS to the Boot2Docker VM. If one of the values in PATHS 831 | # is not valid (e.g. doesn't exist), it will be ignored. 832 | # 833 | # Examples: 834 | # 835 | # rsync /foo /bar 836 | # Result: /foo and /bar are rsync'ed to the Boot2DockerVM 837 | # 838 | function do_sync { 839 | local readonly paths_to_sync=("$@") 840 | local path="" 841 | 842 | for path in "${paths_to_sync[@]}"; do 843 | do_rsync "$path" 844 | done 845 | } 846 | 847 | # 848 | # Usage: create_sync_directories [PATHS ...] 849 | # 850 | # Sets up all necessary parent directories and permissions for PATHS in the 851 | # Boot2Docker VM. 852 | function create_sync_directories { 853 | local readonly paths_to_sync=("$@") 854 | 855 | local dirs_to_create=() 856 | local path="" 857 | 858 | for path in "${paths_to_sync[@]}"; do 859 | local readonly parent_dir=$(dirname "$path") 860 | if [[ "$parent_dir" != "/" ]]; then 861 | dirs_to_create+=("$parent_dir") 862 | fi 863 | done 864 | 865 | local readonly dir_string=$(join " " "${dirs_to_create[@]}") 866 | local readonly mkdir_string="sudo mkdir -p $dir_string" 867 | local readonly chown_string="sudo chown -R $DOCKER_HOST_USER $dir_string" 868 | local readonly ssh_cmd="$mkdir_string && $chown_string" 869 | 870 | log_debug "Creating parent directories in Docker VM: $ssh_cmd" 871 | $DOCKER_HOST_SSH_COMMAND "$ssh_cmd" 872 | } 873 | 874 | # 875 | # Usage: tar_sync [PATHS ...] 876 | # 877 | # Use tar to set up the initial sync of PATHS in the Boot2Docker VM, which 878 | # is faster than letting rsync do it. 879 | function tar_sync { 880 | local readonly paths_to_sync=("$@") 881 | 882 | local path="" 883 | local parent_dir="" 884 | local base_name="" 885 | local readonly excludes=($EXCLUDES) 886 | local readonly exclude_flags=${excludes[@]/#/--exclude } 887 | local readonly includes=($INCLUDES) 888 | local readonly include_flags=${includes[@]/#/--include } 889 | 890 | for path in "${paths_to_sync[@]}"; do 891 | parent_dir=$(dirname "$path") 892 | base_name=$(basename "$path") 893 | if $DOCKER_HOST_SSH_COMMAND "test -e '$parent_dir/$base_name'" > /dev/null 2>&1; then 894 | log_debug "skipped tar for $parent_dir/$base_name" 895 | else 896 | log_info "Initial sync using tar for $parent_dir/$base_name" 897 | tar -cC "$parent_dir" $include_flags $exclude_flags "$base_name" | $DOCKER_HOST_SSH_COMMAND "tar -xC '$parent_dir'" 898 | fi 899 | done 900 | } 901 | 902 | # 903 | # Usage: initial_sync 904 | # 905 | # Perform the initial sync of PATHS_TO_SYNC to the Boot2Docker VM, including 906 | # setting up all necessary parent directories and permissions. 907 | # 908 | function initial_sync { 909 | local readonly paths_to_sync=($PATHS_TO_SYNC) 910 | log_info "Performing initial sync of paths: ${paths_to_sync[@]}" 911 | 912 | create_sync_directories "${paths_to_sync[@]}" 913 | 914 | tar_sync "${paths_to_sync[@]}" 915 | 916 | log_debug "Starting sync paths: $paths_to_sync" 917 | do_sync "${paths_to_sync[@]}" 918 | log_info "Initial sync done" 919 | } 920 | 921 | # 922 | # Usage: watch 923 | # 924 | # Watches the paths in the global variable PATHS_TO_SYNC for changes and rsyncs 925 | # any files that changed. 926 | # 927 | function watch { 928 | log_info "Watching: $PATHS_TO_SYNC" 929 | 930 | local readonly fswatch_cmd="fswatch -0 $PATHS_TO_SYNC" 931 | log_debug "$fswatch_cmd" 932 | 933 | local file="" 934 | eval "$fswatch_cmd" | while read -d "" file 935 | do 936 | do_sync "$file" 937 | done 938 | } 939 | 940 | ################################################################################ 941 | # User input and command line args 942 | ################################################################################ 943 | 944 | # 945 | # Usage: instructions 946 | # 947 | # Prints the usage instructions for this script to stdout. 948 | # 949 | function instructions { 950 | echo -e 951 | echo -e "Usage: docker-osx-dev [COMMAND] [OPTIONS]" 952 | echo -e 953 | echo -e "Commands:" 954 | echo -e " $SYNC_COMMAND\t\tStart file syncing. This is the default if no COMMAND is specified." 955 | echo -e " $WATCH_ONLY_COMMAND\tWatch the file system for changes, without syncing first." 956 | echo -e " $SYNC_ONLY_COMMAND\tSync the file system and exit, without watching afterwords." 957 | echo -e " $INSTALL_COMMAND\tInstall docker-osx-dev and all of its dependencies." 958 | echo -e 959 | echo -e "Options:" 960 | echo -e " -m, --machine-name name\t\tWhen suplied syncs with the given docker machine host" 961 | echo -e " -s, --sync-path PATH\t\t\tSync PATH to the Boot2Docker VM. No wildcards allowed. May be specified multiple times. Default: $DEFAULT_PATHS_TO_SYNC" 962 | echo -e " -e, --exclude-path PATH\t\tExclude PATH while syncing. Behaves identically to rsync's --exclude parameter. May be specified multiple times. Default: $DEFAULT_EXCLUDES" 963 | echo -e " -c, --compose-file COMPOSE_FILE\tRead in this docker-compose file and sync any volumes specified in it. Default: $DEFAULT_COMPOSE_FILE" 964 | echo -e " -i, --ignore-file IGNORE_FILE\t\tRead in this ignore file and exclude any paths within it while syncing (see --exclude). Default: $DEFAULT_IGNORE_FILE" 965 | echo -e " --only-dependencies\t\tDuring install, only install the homebrew dependencies. Useful if homebrew is needs to be run as a different user." 966 | echo -e " --skip-dependencies\t\tDuring install, don't install the homebrew dependencies. Useful if homebrew is needs to be run as a different user." 967 | echo -e " -r, --remove-shared-folders \t\tAutomatically remove Virtualbox shared folders. Shared folders may void any performance benefits from using docker-osx-dev" 968 | echo -e " -l, --log-level LOG_LEVEL\t\tSpecify the logging level. One of: $LOG_LEVELS. Default: ${DEFAULT_LOG_LEVEL}" 969 | echo -e " -h, --help\t\t\t\tPrint this help text and exit." 970 | echo -e 971 | echo -e "Overview:" 972 | echo -e 973 | echo -e "docker-osx-dev is a script you can use to sync folders to the Boot2Docker (or docker-machine) VM using rsync." 974 | echo -e "It's an alternative to using VirtualBox shared folders, which are agonizingly slow and break file watchers." 975 | echo -e "For more info, see: https://github.com/brikis98/docker-osx-dev" 976 | echo -e 977 | echo -e "Example workflow:" 978 | echo -e " > docker-osx-dev -s /host-folder" 979 | echo -e " > docker run -v /host-folder:/guest-folder some-docker-image" 980 | echo -e 981 | echo -e " After you run the commands above, /host-folder on OS X will be kept in sync with /guest-folder in some-docker-image." 982 | echo -e 983 | } 984 | 985 | # 986 | # Usage: load_paths_from_docker_compose DOCKER_COMPOSE_FILE 987 | # 988 | # Parses out all volumes: entries from the docker-compose file 989 | # DOCKER_COMPOSE_FILE. This is a very hacky function that just uses regex 990 | # instead of a proper yaml parser. If it proves to be fragile, it will need to 991 | # be replaced. 992 | # 993 | function load_paths_from_docker_compose { 994 | local readonly yaml_file_path="$1" 995 | local paths=() 996 | 997 | local docker_compose_param_yaml_file="" 998 | if [[ ! -z "${yaml_file_path}" ]]; then 999 | docker_compose_param_yaml_file="-f ${yaml_file_path}" 1000 | fi 1001 | 1002 | # Parse docker-compose YAML configuration 1003 | # Here is an example of output sent by docker-compose config: 1004 | # networks: {} 1005 | # services: 1006 | # db: 1007 | # image: baz/blah 1008 | # network_mode: bridge 1009 | # web: 1010 | # image: foo/bar 1011 | # links: 1012 | # - db 1013 | # network_mode: bridge 1014 | # ports: 1015 | # - 3000:3000 1016 | # volumes: 1017 | # - /host:/guest:rw 1018 | # version: '2.0' 1019 | # volumes: {} 1020 | local in_volumes_block=false 1021 | local line="" 1022 | while read line; do 1023 | if $in_volumes_block; then 1024 | if [[ "${line:0:2}" = "- " ]]; then 1025 | local readonly path=$(echo $line | sed -ne "s/- \(\/[^:]*\):.*$/\1/p") 1026 | if [ ! -z "$path" ]; then 1027 | paths+=("$path") 1028 | fi 1029 | else 1030 | in_volumes_block=false 1031 | fi 1032 | else 1033 | if [[ "$line" = "volumes:" ]]; then 1034 | in_volumes_block=true 1035 | fi 1036 | fi 1037 | done < <(docker-compose ${docker_compose_param_yaml_file} config 2> /dev/null) 1038 | # Do not use a pipe to prevent a subshell 1039 | 1040 | echo "${paths[@]}" 1041 | } 1042 | 1043 | # 1044 | # Usage: load_exclude_paths IGNORE_FILE 1045 | # 1046 | # Parse the paths from IGNORE_FILE that are of the format used by .gitignore and 1047 | # .dockerignore: that is, each line contains a single path, and lines that 1048 | # start with a pound sign are treated as comments. Lines that start with a ! 1049 | # are includes and are also ignored. 1050 | # 1051 | function load_exclude_paths { 1052 | local readonly ignore_file="$1" 1053 | local paths=() 1054 | 1055 | if [[ -f "$ignore_file" ]]; then 1056 | local line="" 1057 | while read line; do 1058 | if [[ "${line:0:1}" != "#" ]] && [[ "${line:0:1}" != "!" ]]; then 1059 | paths+=("$line") 1060 | fi 1061 | done < "$ignore_file" 1062 | fi 1063 | 1064 | echo "${paths[@]}" 1065 | } 1066 | 1067 | # 1068 | # Usage: load_include_paths IGNORE_FILE 1069 | # 1070 | # Parse the paths from IGNORE_FILE that are of the format used by .gitignore and 1071 | # .dockerignore: that is, each line contains a single path, and only lines 1072 | # starting with a ! are kept. 1073 | # 1074 | function load_include_paths { 1075 | local readonly ignore_file="$1" 1076 | local paths=() 1077 | 1078 | if [[ -f "$ignore_file" ]]; then 1079 | local line="" 1080 | while read line; do 1081 | if [[ "${line:0:1}" == "!" ]]; then 1082 | paths+=("${line:1}") 1083 | fi 1084 | done < "$ignore_file" 1085 | fi 1086 | 1087 | echo "${paths[@]}" 1088 | } 1089 | 1090 | # 1091 | # Usage: configure_paths_to_sync COMPOSE_FILE [PATHS_FROM_CMD_LINE ...] 1092 | # 1093 | # Set \the paths that should be synced to the Boot2Docker VM. These 1094 | # paths will be read from the Docker Compose file COMPOSE_FILE as well as paths 1095 | # specified via the command line as PATHS_FROM_CMD_LINE. If no paths are found 1096 | # in either place, this function will fall back to DEFAULT_PATHS_TO_SYNC. 1097 | # 1098 | function configure_paths_to_sync { 1099 | local readonly compose_file="$1" 1100 | shift 1101 | local readonly paths_to_sync_from_cmd_line=("$@") 1102 | local readonly paths_to_sync_from_compose_file=($(load_paths_from_docker_compose "$compose_file")) 1103 | 1104 | local paths_to_sync=() 1105 | if [[ "${#paths_to_sync_from_cmd_line[@]}" -gt 0 ]]; then 1106 | log_info "Using sync paths from command line args: ${paths_to_sync_from_cmd_line[@]}" 1107 | paths_to_sync+=("${paths_to_sync_from_cmd_line[@]}") 1108 | fi 1109 | 1110 | if [[ "${#paths_to_sync_from_compose_file}" -gt 0 ]]; then 1111 | log_info "Using sync paths from Docker Compose file at $compose_file: ${paths_to_sync_from_compose_file[@]}" 1112 | paths_to_sync+=("${paths_to_sync_from_compose_file[@]}") 1113 | fi 1114 | 1115 | if [[ "${#paths_to_sync[@]}" -eq 0 ]]; then 1116 | log_info "Using default sync paths: $DEFAULT_PATHS_TO_SYNC" 1117 | paths_to_sync=($DEFAULT_PATHS_TO_SYNC) 1118 | fi 1119 | 1120 | local normalized_paths_to_sync=() 1121 | local path="" 1122 | for path in "${paths_to_sync[@]}"; do 1123 | local normalized_path=$(greadlink -m $(eval echo "$path")) 1124 | normalized_paths_to_sync+=("$normalized_path") 1125 | done 1126 | 1127 | PATHS_TO_SYNC="${normalized_paths_to_sync[@]}" 1128 | log_info "Complete list of paths to sync: $PATHS_TO_SYNC" 1129 | } 1130 | 1131 | # 1132 | # Usage: configure_excludes IGNORE_FILE [EXCLUDE_PATHS_FROM_CMD_LINE ...] 1133 | # 1134 | # Sets the paths that should be excluded when syncing files to the Boot2Docker 1135 | # VM. EXCLUDE_PATHS_FROM_CMD_LINE are paths specified as command line arguments 1136 | # and will take precedence. If none are specified, this function will try to 1137 | # read the ignore file (see load_exclude_paths) at IGNORE_FILE and use those 1138 | # entries as excludes. If that fails, this function will fall back to 1139 | # DEFAULT_EXCLUDES. 1140 | # 1141 | function configure_excludes { 1142 | local readonly ignore_file="$1" 1143 | shift 1144 | local readonly excludes_from_cmd_line=("$@") 1145 | local readonly excludes_from_ignore_file=($(load_exclude_paths "$ignore_file")) 1146 | 1147 | local excludes=() 1148 | if [[ "${#excludes_from_cmd_line}" -gt 0 ]]; then 1149 | log_info "Using exclude paths from command line args: ${excludes_from_cmd_line[@]}" 1150 | excludes+=("${excludes_from_cmd_line[@]}") 1151 | fi 1152 | 1153 | if [[ "${#excludes_from_ignore_file}" -gt 0 ]]; then 1154 | log_info "Using excludes from ignore file $ignore_file: ${excludes_from_ignore_file[@]}" 1155 | excludes+=("${excludes_from_ignore_file[@]}") 1156 | fi 1157 | 1158 | if [[ "${#excludes[@]}" -eq 0 ]]; then 1159 | log_info "Using default exclude paths: $DEFAULT_EXCLUDES" 1160 | excludes=($DEFAULT_EXCLUDES) 1161 | fi 1162 | 1163 | EXCLUDES="${excludes[@]}" 1164 | log_info "Complete list of paths to exclude: $EXCLUDES" 1165 | } 1166 | 1167 | # 1168 | # Usage: configure_includes IGNORE_FILE [INCLUDE_PATHS_FROM_CMD_LINE ...] 1169 | # 1170 | # Sets the paths that should be included when syncing files to the Boot2Docker 1171 | # VM. INCLUDE_PATHS_FROM_CMD_LINE are paths specified as command line arguments 1172 | # and will take precedence. If none are specified, this function will try to 1173 | # read the ignore file (see load_include_paths) at IGNORE_FILE and use those 1174 | # entries as includes. 1175 | # 1176 | function configure_includes { 1177 | local readonly ignore_file="$1" 1178 | shift 1179 | local readonly includes_from_cmd_line=("$@") 1180 | local readonly includes_from_ignore_file=($(load_include_paths "$ignore_file")) 1181 | 1182 | local includes=() 1183 | if [[ "${#includes_from_cmd_line}" -gt 0 ]]; then 1184 | log_info "Using include paths from command line args: ${includes_from_cmd_line[@]}" 1185 | includes+=("${includes_from_cmd_line[@]}") 1186 | fi 1187 | 1188 | if [[ "${#includes_from_ignore_file}" -gt 0 ]]; then 1189 | log_info "Using includes from ignore file $ignore_file: ${includes_from_ignore_file[@]}" 1190 | includes+=("${includes_from_ignore_file[@]}") 1191 | fi 1192 | 1193 | INCLUDES="${includes[@]}" 1194 | log_info "Complete list of paths to include: $INCLUDES" 1195 | } 1196 | 1197 | # 1198 | # Usage: sync REMOVE_SHARED_FOLDERS 1199 | # 1200 | # Runs the docker-osx-dev script to to sync files. 1201 | # 1202 | function sync { 1203 | local readonly remove_shared_folders=$1 1204 | 1205 | log_info "Starting docker-osx-dev file syncing" 1206 | init_docker_host "$remove_shared_folders" 1207 | install_rsync_on_docker_host 1208 | initial_sync 1209 | } 1210 | 1211 | # 1212 | # Usage: install [SKIP_DEPENDENCIES] [ONLY_DEPENDENCIES] [REMOVE_SHARED_FOLDERS] 1213 | # 1214 | # Installs the docker-osx-dev script, all of its dependencies, and configures 1215 | # the environment. 1216 | # 1217 | function install { 1218 | log_info "Starting install of docker-osx-dev" 1219 | local readonly skip_dependencies=$1 1220 | local readonly only_dependencies=$2 1221 | local readonly remove_shared_folders=$3 1222 | if test -z "$skip_dependencies" ; then 1223 | install_dependencies 1224 | fi 1225 | if test -z "$only_dependencies" ; then 1226 | init_docker_host "$remove_shared_folders" 1227 | install_rsync_on_docker_host 1228 | add_docker_host 1229 | add_environment_variables 1230 | print_next_steps 1231 | fi 1232 | } 1233 | 1234 | # 1235 | # Executes no code or side effects. Used only at test time to make it easy to 1236 | # "source" this script. 1237 | # 1238 | function test_mode { 1239 | return 0 1240 | } 1241 | 1242 | # 1243 | # Usage: assert_valid_arg ARG ARG_NAME 1244 | # 1245 | # Asserts that ARG is not empty and is not a flag (i.e. starts with a - or --) 1246 | # 1247 | # Examples: 1248 | # 1249 | # assert_valid_arg "foo" "--my-arg" 1250 | # returns 0 1251 | # 1252 | # assert_valid_arg "" "--my-arg" 1253 | # prints error, instructions, and exits with error code 1 1254 | # 1255 | # assert_valid_arg "--foo" "--my-arg" 1256 | # prints error, instructions, and exits with error code 1 1257 | # 1258 | function assert_valid_arg { 1259 | local readonly arg="$1" 1260 | local readonly arg_name="$2" 1261 | 1262 | if [[ -z "$arg" || "${arg:0:1}" = "-" ]]; then 1263 | log_error "You must provide a value for argument $arg_name" 1264 | instructions 1265 | exit 1 1266 | fi 1267 | } 1268 | 1269 | # 1270 | # Usage handle_command ARGS ... 1271 | # 1272 | # Parses ARGS to kick off this script. See the output of the instructions 1273 | # function for details. 1274 | # 1275 | function handle_command { 1276 | check_prerequisites 1277 | 1278 | local cmd="$DEFAULT_COMMAND" 1279 | local log_level="$DEFAULT_LOG_LEVEL" 1280 | local docker_compose_file="$DEFAULT_COMPOSE_FILE" 1281 | local ignore_file="$DEFAULT_IGNORE_FILE" 1282 | local paths_to_sync=() 1283 | local excludes=() 1284 | local includes=() 1285 | 1286 | while [[ $# > 0 ]]; do 1287 | key="$1" 1288 | 1289 | case $key in 1290 | "$SYNC_COMMAND") 1291 | cmd="$SYNC_COMMAND" 1292 | ;; 1293 | "$SYNC_ONLY_COMMAND") 1294 | cmd="$SYNC_ONLY_COMMAND" 1295 | ;; 1296 | "$WATCH_ONLY_COMMAND") 1297 | cmd="$WATCH_ONLY_COMMAND" 1298 | ;; 1299 | "$INSTALL_COMMAND") 1300 | cmd="$INSTALL_COMMAND" 1301 | ;; 1302 | "$TEST_COMMAND") 1303 | cmd="$TEST_COMMAND" 1304 | ;; 1305 | -s|--sync-path) 1306 | assert_valid_arg "$2" "$key" 1307 | paths_to_sync+=("$2") 1308 | shift 1309 | ;; 1310 | -e|--exclude-path) 1311 | assert_valid_arg "$2" "$key" 1312 | excludes+=("$2") 1313 | shift 1314 | ;; 1315 | -I|--include-path) 1316 | assert_valid_arg "$2" "$key" 1317 | includes+=("$2") 1318 | shift 1319 | ;; 1320 | -c|--compose-file) 1321 | assert_valid_arg "$2" "$key" 1322 | docker_compose_file="$2" 1323 | shift 1324 | ;; 1325 | -i|--ignore-file) 1326 | assert_valid_arg "$2" "$key" 1327 | ignore_file="$2" 1328 | shift 1329 | ;; 1330 | -l|--log-level) 1331 | assert_valid_arg "$2" "$key" 1332 | log_level="$2" 1333 | shift 1334 | ;; 1335 | -m|--machine-name) 1336 | assert_valid_arg "$2" "$key" 1337 | DOCKER_MACHINE_NAME="$2" 1338 | shift 1339 | ;; 1340 | -r|--remove-shared-folders) 1341 | local readonly remove_shared_folders=true 1342 | ;; 1343 | --only-dependencies) 1344 | local readonly only_dependencies=true 1345 | ;; 1346 | --skip-dependencies) 1347 | local readonly skip_dependencies=true 1348 | ;; 1349 | -h|--help) 1350 | instructions 1351 | exit 0 1352 | ;; 1353 | *) 1354 | log_error "Unrecognized argument: $key" 1355 | instructions 1356 | exit 1 1357 | ;; 1358 | esac 1359 | 1360 | shift 1361 | done 1362 | 1363 | assert_mutually_exclusive "--only-dependencies and --skip-dependencies are mutually exclusive" "$only_dependencies" "$skip_dependencies" 1364 | 1365 | case "$cmd" in 1366 | "$SYNC_COMMAND" | "$SYNC_ONLY_COMMAND" | "$WATCH_ONLY_COMMAND") 1367 | configure_log_level "$log_level" 1368 | configure_paths_to_sync "$docker_compose_file" "${paths_to_sync[@]}" 1369 | configure_excludes "$ignore_file" "${excludes[@]}" 1370 | configure_includes "$ignore_file" "${includes[@]}" 1371 | case "$cmd" in 1372 | "$SYNC_COMMAND") 1373 | sync "$remove_shared_folders" 1374 | watch 1375 | ;; 1376 | "$SYNC_ONLY_COMMAND") 1377 | sync "$remove_shared_folders" 1378 | ;; 1379 | "$WATCH_ONLY_COMMAND") 1380 | watch 1381 | ;; 1382 | esac 1383 | ;; 1384 | "$INSTALL_COMMAND") 1385 | configure_log_level "$log_level" 1386 | install "$skip_dependencies" "$only_dependencies" "$remove_shared_folders" 1387 | ;; 1388 | "$TEST_COMMAND") 1389 | test_mode 1390 | ;; 1391 | *) 1392 | log_error "Internal error: unrecognized command $cmd" 1393 | exit 1 1394 | ;; 1395 | esac 1396 | } 1397 | 1398 | handle_command "$@" 1399 | -------------------------------------------------------------------------------- /test/docker-osx-dev.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | # 3 | # Unit tests for docker-osx-dev. To run these tests, you must have bats 4 | # installed. See https://github.com/sstephenson/bats 5 | 6 | source src/docker-osx-dev "test_mode" 7 | load test_helper 8 | 9 | 10 | @test "index_of doesn't find match in empty array" { 11 | array=() 12 | run index_of "foo" "${array[@]}" 13 | assert_output -1 14 | } 15 | 16 | @test "index_of finds match in 1 item array" { 17 | array=("foo") 18 | run index_of "foo" "${array[@]}" 19 | assert_output 0 20 | } 21 | 22 | @test "index_of doesn't find match in 1 item array" { 23 | array=("abc") 24 | run index_of "foo" "${array[@]}" 25 | assert_output -1 26 | } 27 | 28 | @test "index_of finds match in 3 item array" { 29 | array=("abc" "foo" "def") 30 | run index_of "foo" "${array[@]}" 31 | assert_output 1 32 | } 33 | 34 | @test "index_of doesn't find match in 3 item array" { 35 | array=("abc" "def" "ghi") 36 | run index_of "foo" "${array[@]}" 37 | assert_output -1 38 | } 39 | 40 | @test "index_of finds match with multi argument syntax" { 41 | run index_of "foo" "abc" "def" "ghi" "foo" 42 | assert_output 3 43 | } 44 | 45 | @test "index_of returns index of first match" { 46 | run index_of foo abc foo def ghi foo 47 | assert_output 1 48 | } 49 | 50 | @test "log called with color, log level, and message prints to stdout" { 51 | run log "color_start" "color_end" "timestamp" "$LOG_LEVEL_INFO" "foo" 52 | assert_output "color_starttimestamp [$LOG_LEVEL_INFO] foocolor_end" 53 | } 54 | 55 | @test "log called with color, log level, and multiple messages prints them all to stdout" { 56 | run log "color_start" "color_end" "timestamp" "$LOG_LEVEL_INFO" "foo" "bar" "baz" 57 | assert_output "color_starttimestamp [$LOG_LEVEL_INFO] foo bar bazcolor_end" 58 | } 59 | 60 | @test "log called with color and log level reads message from stdin stdout" { 61 | result=$(echo "foo" | log "color_start" "color_end" "timestamp" "$LOG_LEVEL_INFO") 62 | assert_equal "color_starttimestamp [$LOG_LEVEL_INFO] foocolor_end" "$result" 63 | } 64 | 65 | @test "log called with disabled log level prints nothing to stdout" { 66 | run log "color_start" "color_end" "timestamp" "$LOG_LEVEL_DEBUG" "foo" 67 | assert_output "" 68 | } 69 | 70 | @test "join empty arrays" { 71 | run join "," 72 | assert_output "" 73 | } 74 | 75 | @test "join arrays of length 1" { 76 | run join "," "foo" 77 | assert_output "foo" 78 | } 79 | 80 | @test "join arrays of length 3" { 81 | run join ", " "foo" "bar" "baz" 82 | assert_output "foo, bar, baz" 83 | } 84 | 85 | @test "join arrays with empty separator" { 86 | run join "" "foo" "bar" "baz" 87 | assert_output "foobarbaz" 88 | } 89 | 90 | @test "join arrays passed in as arguments" { 91 | arr=(foo bar baz) 92 | run join ", " "${arr[@]}" 93 | assert_output "foo, bar, baz" 94 | } 95 | 96 | @test "assert_valid_log_level accepts DEBUG" { 97 | run assert_valid_log_level "$LOG_LEVEL_DEBUG" 98 | assert_success 99 | } 100 | 101 | @test "assert_valid_log_level rejects an invalid value" { 102 | run assert_valid_log_level "INVALID_LOG_LEVEL" 103 | assert_failure 104 | } 105 | 106 | @test "configure_paths_to_sync with non-existent docker-compose file results in syncing the current directory" { 107 | configure_paths_to_sync not-a-real-docker-compose-file > /dev/null 108 | assert_equal "$(pwd)" "$PATHS_TO_SYNC" 109 | } 110 | 111 | @test "configure_paths_to_sync with docker-compose file with no volumes results in syncing the current directory" { 112 | configure_paths_to_sync test/resources/docker-compose-no-volumes.yml > /dev/null 113 | assert_equal "$(pwd)" "$PATHS_TO_SYNC" 114 | } 115 | 116 | @test "configure_paths_to_sync reads paths to sync from docker-compose file" { 117 | configure_paths_to_sync "test/resources/docker-compose-one-volume.yml" > /dev/null 118 | assert_equal "/host" "$PATHS_TO_SYNC" 119 | } 120 | 121 | @test "configure_paths_to_sync reads paths to sync from docker-compose file that uses double quotes around value" { 122 | configure_paths_to_sync "test/resources/docker-compose-one-volume-double-quotes.yml" > /dev/null 123 | assert_equal "/host" "$PATHS_TO_SYNC" 124 | } 125 | 126 | @test "configure_paths_to_sync reads paths to sync from docker-compose file that uses single quotes around value" { 127 | configure_paths_to_sync "test/resources/docker-compose-one-volume-single-quotes.yml" > /dev/null 128 | assert_equal "/host" "$PATHS_TO_SYNC" 129 | } 130 | 131 | @test "configure_paths_to_sync reads paths to sync from docker-compose file that uses extends" { 132 | configure_paths_to_sync "test/resources/docker-compose-extends.yml" > /dev/null 133 | assert_equal "/host /host2" "$PATHS_TO_SYNC" 134 | } 135 | 136 | @test "configure_paths_to_sync reads paths to sync from docker-compose file that uses named volumes" { 137 | configure_paths_to_sync "test/resources/docker-compose-named-volumes.yml" > /dev/null 138 | assert_equal "/host" "$PATHS_TO_SYNC" 139 | } 140 | 141 | @test "configure_paths_to_sync with one path from command line" { 142 | configure_paths_to_sync "test/resources/docker-compose-no-volumes.yml" "/foo" > /dev/null 143 | assert_equal "/foo" "$PATHS_TO_SYNC" 144 | } 145 | 146 | @test "configure_paths_to_sync with multiple paths from command line" { 147 | configure_paths_to_sync "test/resources/docker-compose-no-volumes.yml" "/foo" "/bar" "/baz/blah" > /dev/null 148 | assert_equal "/foo /bar /baz/blah" "$PATHS_TO_SYNC" 149 | } 150 | 151 | @test "configure_paths_to_sync with multiple paths from command line and paths from docker-compose.yml" { 152 | configure_paths_to_sync "test/resources/docker-compose-one-volume.yml" "/foo" "/bar" "/baz/blah" > /dev/null 153 | assert_equal "/foo /bar /baz/blah /host" "$PATHS_TO_SYNC" 154 | } 155 | 156 | @test "configure_paths_to_sync expands tildes correctly" { 157 | configure_paths_to_sync not-a-real-docker-compose-file "~/foo" > /dev/null 158 | assert_equal "$HOME/foo" "$PATHS_TO_SYNC" 159 | } 160 | 161 | @test "configure_paths_to_sync correctly reads paths with access modifier" { 162 | configure_paths_to_sync "test/resources/docker-compose-one-volume-access-modifier.yml" > /dev/null 163 | assert_equal "/host" "$PATHS_TO_SYNC" 164 | } 165 | 166 | @test "configure_excludes with non-existent ignore file results in default excludes" { 167 | configure_excludes "not-a-real-ignore-file" > /dev/null 168 | assert_equal "$DEFAULT_EXCLUDES" "$EXCLUDES" 169 | } 170 | 171 | @test "configure_excludes with empty ignore file results in default excludes" { 172 | configure_excludes "test/resources/ignore-file-empty.txt" > /dev/null 173 | assert_equal "$DEFAULT_EXCLUDES" "$EXCLUDES" 174 | } 175 | 176 | @test "configure_excludes loads ignores from ignore file" { 177 | configure_excludes "test/resources/ignore-file-with-one-entry.txt" > /dev/null 178 | assert_equal "foo" "$EXCLUDES" 179 | } 180 | 181 | @test "configure_excludes uses single command line arg" { 182 | configure_excludes "not-a-real-ignore-file" "foo" > /dev/null 183 | assert_equal "foo" "$EXCLUDES" 184 | } 185 | 186 | @test "configure_excludes uses multiple command line args" { 187 | configure_excludes "not-a-real-ignore-file" "foo" "bar" "baz" > /dev/null 188 | assert_equal "foo bar baz" "$EXCLUDES" 189 | } 190 | 191 | @test "configure_excludes uses ignore file and multiple command line args" { 192 | configure_excludes "test/resources/ignore-file-with-one-entry.txt" "foo" "bar" "baz" > /dev/null 193 | assert_equal "foo bar baz foo" "$EXCLUDES" 194 | } 195 | 196 | @test "configure_includes with non-existent ignore file results in no includes" { 197 | configure_includes "not-a-real-ignore-file" > /dev/null 198 | assert_equal "" "$INCLUDES" 199 | } 200 | 201 | @test "configure_includes with empty ignore file results in no includes" { 202 | configure_includes "test/resources/ignore-file-empty.txt" > /dev/null 203 | assert_equal "" "$INCLUDES" 204 | } 205 | 206 | @test "configure_includes with ignore file with no includes results in no includes" { 207 | configure_includes "test/resources/ignore-file-with-one-entry.txt" > /dev/null 208 | assert_equal "" "$INCLUDES" 209 | } 210 | 211 | @test "configure_includes loads includes from ignore file" { 212 | configure_includes "test/resources/ignore-file-with-includes.txt" > /dev/null 213 | assert_equal "bar foo" "$INCLUDES" 214 | } 215 | 216 | @test "configure_includes uses single command line arg" { 217 | configure_includes "not-a-real-ignore-file" "foo" > /dev/null 218 | assert_equal "foo" "$INCLUDES" 219 | } 220 | 221 | @test "configure_includes uses multiple command line args" { 222 | configure_includes "not-a-real-ignore-file" "foo" "bar" "baz" > /dev/null 223 | assert_equal "foo bar baz" "$INCLUDES" 224 | } 225 | 226 | @test "configure_includes uses ignore file and multiple command line args" { 227 | configure_includes "test/resources/ignore-file-with-includes.txt" "abc" "def" "ghi" > /dev/null 228 | assert_equal "abc def ghi bar foo" "$INCLUDES" 229 | } 230 | 231 | @test "load_exclude_paths skips non-existent files" { 232 | run load_exclude_paths "not-a-real-file" 233 | assert_output "" 234 | } 235 | 236 | @test "load_exclude_paths handles empty ignore files" { 237 | run load_exclude_paths "test/resources/ignore-file-empty.txt" 238 | assert_output "" 239 | } 240 | 241 | @test "load_exclude_paths handles ignore file with one entry" { 242 | run load_exclude_paths "test/resources/ignore-file-with-one-entry.txt" 243 | assert_output "foo" 244 | } 245 | 246 | @test "load_exclude_paths handles ignore file with multiple entries" { 247 | run load_exclude_paths "test/resources/ignore-file-with-multiple-entries.txt" 248 | assert_output "foo bar baz" 249 | } 250 | 251 | @test "load_exclude_paths handles ignore file with comments" { 252 | run load_exclude_paths "test/resources/ignore-file-with-comments.txt" 253 | assert_output "foo bar baz" 254 | } 255 | 256 | @test "load_exclude_paths handles ignore file with includes" { 257 | run load_exclude_paths "test/resources/ignore-file-with-includes.txt" 258 | assert_output "foo bar baz" 259 | } 260 | 261 | @test "load_include_paths skips non-existent files" { 262 | run load_include_paths "not-a-real-file" 263 | assert_output "" 264 | } 265 | 266 | @test "load_include_paths handles empty ignore files" { 267 | run load_include_paths "test/resources/ignore-file-empty.txt" 268 | assert_output "" 269 | } 270 | 271 | @test "load_include_paths handles ignore file with includes" { 272 | run load_include_paths "test/resources/ignore-file-with-includes.txt" 273 | assert_output "bar foo" 274 | } 275 | 276 | @test "load_paths_from_docker_compose skips non-existent files" { 277 | run load_paths_from_docker_compose "not-a-real-file" 278 | assert_output "" 279 | } 280 | 281 | @test "load_paths_from_docker_compose handles docker compose files with no volumes" { 282 | run load_paths_from_docker_compose "test/resources/docker-compose-no-volumes.yml" 283 | assert_output "" 284 | } 285 | 286 | @test "load_paths_from_docker_compose handles docker compose files with one volume" { 287 | run load_paths_from_docker_compose "test/resources/docker-compose-one-volume.yml" 288 | assert_output "/host" 289 | } 290 | 291 | @test "load_paths_from_docker_compose handles docker compose files with multiple volumes" { 292 | run load_paths_from_docker_compose "test/resources/docker-compose-multiple-volumes.yml" 293 | assert_output "/host1 /foo/bar/baz /source/path" 294 | } 295 | 296 | @test "load_paths_from_docker_compose handles docker compose files with multiple containers and multiple volumes" { 297 | run load_paths_from_docker_compose "test/resources/docker-compose-multiple-containers-with-volumes.yml" 298 | assert_output "/ /foo/bar /host1 /host2" # docker-compose config sorts services alphabetically 299 | } 300 | 301 | @test "load_paths_from_docker_compose handles docker compose files with non-mounted volumes" { 302 | run load_paths_from_docker_compose "test/resources/docker-compose-non-mounted-volumes.yml" 303 | assert_output "/host /a" 304 | } 305 | 306 | @test "assert_non_empty exits on empty value" { 307 | run assert_non_empty "" 308 | assert_failure 309 | } 310 | 311 | @test "assert_non_empty doesn't exit on non-empty value" { 312 | run assert_non_empty "foo" 313 | assert_success 314 | } 315 | 316 | @test "assert_mutually_exclusive exits on conflicting variables" { 317 | local readonly foo=1 318 | local readonly bar=2 319 | run assert_mutually_exclusive "error message" "$foo" "$bar" 320 | assert_failure 321 | } 322 | 323 | @test "assert_mutually_exclusive doesn't exit on conflicting but empty variables" { 324 | local readonly foo= 325 | local readonly bar= 326 | run assert_mutually_exclusive "error message" "$foo" "$bar" 327 | assert_success 328 | } 329 | 330 | @test "assert_mutually_exclusive doesn't exit without any variables" { 331 | run assert_mutually_exclusive "error message" "$foo" "$bar" 332 | assert_success 333 | } 334 | 335 | @test "assert_mutually_exclusive doesn't exit with only the first variable" { 336 | local readonly foo=1 337 | run assert_mutually_exclusive "error message" "$foo" "$bar" 338 | assert_success 339 | } 340 | 341 | @test "assert_mutually_exclusive doesn't exit with only the last variable" { 342 | local readonly bar=2 343 | run assert_mutually_exclusive "error message" "$foo" "$bar" 344 | assert_success 345 | } 346 | 347 | @test "env_is_defined returns true for USER variable being defined" { 348 | run env_is_defined "USER" 349 | assert_success 350 | } 351 | 352 | @test "env_is_defined returns false for non-existent variable being defined" { 353 | run env_is_defined "not-a-real-environment-variable" 354 | assert_failure 355 | } 356 | 357 | @test "determine_boot2docker_exports_for_env_file handles empty string" { 358 | run determine_boot2docker_exports_for_env_file 359 | assert_output "" 360 | } 361 | 362 | @test "determine_boot2docker_exports_for_env_file shows an error for an unexpected boot2docker shellinit output" { 363 | run determine_boot2docker_exports_for_env_file "not-a-valid-shellinit-format" 364 | assert_failure 365 | } 366 | 367 | @test "determine_boot2docker_exports_for_env_file parses a single new export" { 368 | shellinit="export NEW_ENV_VARIABLE=VALUE" 369 | run determine_boot2docker_exports_for_env_file "$shellinit" 370 | assert_output "$(echo -e "$ENV_FILE_COMMENT$shellinit")" 371 | } 372 | 373 | @test "determine_boot2docker_exports_for_env_file parses multiple new exports" { 374 | shellinit="export NEW_ENV_VARIABLE_1=VALUE1 375 | export NEW_ENV_VARIABLE_2=VALUE2 376 | export NEW_ENV_VARIABLE_3=VALUE3" 377 | run determine_boot2docker_exports_for_env_file "$shellinit" 378 | assert_output "$(echo -e "$ENV_FILE_COMMENT$shellinit")" 379 | } 380 | 381 | @test "determine_boot2docker_exports_for_env_file skips environment variables already defined" { 382 | shellinit=" 383 | export USER=$USER 384 | export HOME=$HOME" 385 | run determine_boot2docker_exports_for_env_file "$shellinit" 386 | assert_output "" 387 | } 388 | 389 | @test "determine_boot2docker_exports_for_env_file parses multiple new exports and skips environment variables already defined" { 390 | shellinit=" 391 | export NEW_ENV_VARIABLE_1=VALUE1 392 | export USER=$USER 393 | export HOME=$HOME 394 | export NEW_ENV_VARIABLE_2=VALUE2" 395 | run determine_boot2docker_exports_for_env_file "$shellinit" 396 | assert_output "$(echo -e "${ENV_FILE_COMMENT}export NEW_ENV_VARIABLE_1=VALUE1\nexport NEW_ENV_VARIABLE_2=VALUE2")" 397 | } 398 | 399 | @test "assert_valid_arg empty string is not valid" { 400 | run assert_valid_arg "" "--foo" 401 | assert_failure 402 | } 403 | 404 | @test "assert_valid_arg parameter that starts with a dash is not valid" { 405 | run assert_valid_arg "-b" "--foo" 406 | assert_failure 407 | } 408 | 409 | @test "assert_valid_arg parameter that starts with two dashes is not valid" { 410 | run assert_valid_arg "--bar" "--foo" 411 | assert_failure 412 | } 413 | 414 | @test "assert_valid_arg normal string is valid" { 415 | run assert_valid_arg "normal-string" "--foo" 416 | assert_success 417 | } 418 | 419 | @test "find_path_to_sync_parent should find exact matches" { 420 | export PATHS_TO_SYNC="/foo" 421 | run find_path_to_sync_parent "/foo" 422 | assert_output '/foo' 423 | } 424 | 425 | @test "find_path_to_sync_parent should not find unmatched paths" { 426 | export PATHS_TO_SYNC="/foo" 427 | run find_path_to_sync_parent "/bar" 428 | assert_output '' 429 | } 430 | 431 | @test "find_path_to_sync_parent should find nested matches" { 432 | export PATHS_TO_SYNC="/foo" 433 | run find_path_to_sync_parent "/foo/bar" 434 | assert_output '/foo' 435 | } 436 | 437 | @test "find_path_to_sync_parent should not confuse substring matches" { 438 | export PATHS_TO_SYNC="/foo /bar" 439 | run find_path_to_sync_parent "/bar/foo" 440 | assert_output '/bar' 441 | } 442 | 443 | @test "find_path_to_sync_parent should not find other paths which are substrings" { 444 | export PATHS_TO_SYNC="/some/path /some/path2" 445 | run find_path_to_sync_parent "/some/path2" 446 | assert_output '/some/path2' 447 | } 448 | 449 | @test "find_path_to_sync_parent should not match nested paths against other paths which are substrings" { 450 | export PATHS_TO_SYNC="/some/path /some/path2" 451 | run find_path_to_sync_parent "/some/path2/foo" 452 | assert_output '/some/path2' 453 | } 454 | 455 | @test "find_path_to_sync_parent should match paths starting with a dot" { 456 | export PATHS_TO_SYNC="/some/path" 457 | run find_path_to_sync_parent "/some/path/.git/foo" 458 | assert_output '/some/path' 459 | } 460 | 461 | @test "find_path_to_sync_parent should match paths with weird characters" { 462 | export PATHS_TO_SYNC='/some/path2()$HI' 463 | run find_path_to_sync_parent '/some/path2()$HI/foo' 464 | assert_output '/some/path2()$HI' 465 | } 466 | 467 | @test "init_docker_host should call configure_boot2docker set DOCKER_HOST vars" { 468 | unset DOCKER_HOST_NAME 469 | stub boot2docker 'echo "SSHKey = \"/Users/someone/.ssh/id_boot2docker\""' 470 | export PATH="$BATS_TEST_DIRNAME/stub:$PATH" 471 | 472 | init_docker_host 473 | 474 | assert_equal "docker" "$DOCKER_HOST_USER" 475 | assert_equal "docker@dockerhost" "$DOCKER_HOST_SSH_URL" 476 | assert_equal "boot2docker ssh" "$DOCKER_HOST_SSH_COMMAND" 477 | assert_equal "/Users/someone/.ssh/id_boot2docker" "$DOCKER_HOST_SSH_KEY" 478 | rm_stubs 479 | } 480 | 481 | @test "configure_docker_machine should set DOCKER_HOST vars" { 482 | export DOCKER_MACHINE_NAME="some-machine" 483 | # docker-machine will allways output DOCKER_INSPECT_OUTPUT 484 | # although it would be good to stub each subcommand/param 485 | stub docker-machine "echo 'DOCKER_INSPECT_OUTPUT'" 486 | export PATH="$BATS_TEST_DIRNAME/stub:$PATH" 487 | 488 | configure_docker_machine 489 | 490 | assert_equal "some-machine" "$DOCKER_HOST_NAME" 491 | assert_equal "DOCKER_INSPECT_OUTPUT" "$DOCKER_HOST_USER" 492 | assert_equal "DOCKER_INSPECT_OUTPUT" "$DOCKER_HOST_IP" 493 | 494 | assert_equal "DOCKER_INSPECT_OUTPUT/id_rsa" "$DOCKER_HOST_SSH_KEY" 495 | assert_equal "DOCKER_INSPECT_OUTPUT@DOCKER_INSPECT_OUTPUT" "$DOCKER_HOST_SSH_URL" 496 | assert_equal "docker-machine ssh some-machine" "$DOCKER_HOST_SSH_COMMAND" 497 | rm_stubs 498 | } 499 | 500 | @test "init_boot2docker should check for and unmount VirtualBox shared folders" { 501 | stub boot2docker ' 502 | case "$@" in 503 | "ssh mount") 504 | echo "none on /Users type vboxsf (rw,nodev,relatime)" 505 | ;; 506 | "ssh sudo umount "*) 507 | echo "[TEST] boot2docker $@" 508 | ;; 509 | esac 510 | ' 511 | export PATH="$BATS_TEST_DIRNAME/stub:$PATH" 512 | 513 | run eval 'yes | init_boot2docker' 514 | 515 | assert_line "[TEST] boot2docker ssh sudo umount /Users" 516 | 517 | rm_stubs 518 | } 519 | 520 | @test "brew_install should install things that fail existence_test" { 521 | export -f stub 522 | export BATS_TEST_DIRNAME 523 | 524 | stub brew ' 525 | case "$1" in 526 | list) 527 | exit 1 528 | ;; 529 | install) 530 | echo brew $@ 531 | stub "$2" "echo $2" 532 | ;; 533 | *) 534 | echo brew $@ 535 | ;; 536 | esac 537 | ' 538 | 539 | export PATH="$BATS_TEST_DIRNAME/stub:$PATH" 540 | 541 | run brew_install bleh Bleh 'type bleh' false 542 | 543 | assert_line "brew install bleh" 544 | 545 | rm_stubs 546 | } 547 | 548 | @test "brew_install should not install things that pass existence_test" { 549 | stub brew ' 550 | case "$1" in 551 | list) 552 | if [[ "$2" == "bleh" ]]; then 553 | echo "bleh" 554 | else 555 | exit 1 556 | fi 557 | *) 558 | echo brew $@ 559 | ;; 560 | esac 561 | ' 562 | 563 | stub bleh 'echo bleh' 564 | export PATH="$BATS_TEST_DIRNAME/stub:$PATH" 565 | 566 | run brew_install bleh Bleh 'type bleh' false 567 | 568 | refute_line "brew install bleh" 569 | 570 | rm_stubs 571 | } 572 | -------------------------------------------------------------------------------- /test/integration-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Automated tests for docker-osx-dev 4 | 5 | #set -e 6 | 7 | # Test file constants 8 | readonly TEST_FOLDER="test-project" 9 | readonly TEST_FILE="test-file" 10 | readonly TEST_FILE_CONTENTS="test file contents" 11 | readonly TEST_IMAGE="gliderlabs/alpine:3.2" 12 | 13 | # Console colors 14 | readonly COLOR_INFO='\033[0;3m[TEST_INFO]' 15 | readonly COLOR_WARN='\033[1;33m[TEST_WARN]' 16 | readonly COLOR_ERROR='\033[0;31m[TEST_ERROR]' 17 | readonly COLOR_END='\033[0m' 18 | 19 | # Docker Machine constants 20 | readonly VM_NAME='docker-osx-dev-test' 21 | 22 | function log_info { 23 | log "$1" $COLOR_INFO 24 | } 25 | 26 | function log_warn { 27 | log "$1" $COLOR_WARN 28 | } 29 | 30 | function log_error { 31 | log "$1" $COLOR_ERROR 32 | } 33 | 34 | function log { 35 | local readonly message=$1 36 | local readonly color=$2 || $COLOR_INFO 37 | echo -e "${color} ${message}${COLOR_END}" 38 | } 39 | 40 | function assert_equals { 41 | local readonly left=$1 42 | local readonly right=$2 43 | 44 | if [[ "$left" -ne "$right" ]]; then 45 | echo "Assertion failure: $left != $right" 46 | exit 1 47 | fi 48 | } 49 | 50 | function cleanup { 51 | log_info "Cleaning up old test environment" 52 | if [[ -n $(docker-machine ls | grep -o "^$VM_NAME") ]]; then 53 | log_info "Removing old machine" 54 | docker-machine rm "$VM_NAME" 55 | else 56 | log_info "No old machine found" 57 | fi 58 | } 59 | 60 | function create_machine { 61 | log_info "Creating machine" 62 | docker-machine create "$VM_NAME" --driver=virtualbox 63 | } 64 | 65 | function start_machine { 66 | log_info "Starting machine" 67 | eval $(docker-machine env "$VM_NAME") 68 | docker-machine start "$VM_NAME" 69 | } 70 | 71 | function test_setup { 72 | log_info "Testing the install command" 73 | # We're just looking for the script to run without errors 74 | ./src/docker-osx-dev install 75 | } 76 | 77 | function create_test_project { 78 | log_info "Creating test project in $TEST_FOLDER" 79 | mkdir "$TEST_FOLDER" 80 | cd "$TEST_FOLDER" 81 | echo "$TEST_FILE_CONTENTS" > "$TEST_FILE" 82 | } 83 | 84 | 85 | function test_docker_osx_dev { 86 | log_info "Running docker-osx-dev" 87 | 88 | # This should start syncing in the background 89 | docker-osx-dev & 90 | } 91 | 92 | function test_docker_run { 93 | log_info "Testing docker run with Alpine Linux image" 94 | local readonly out=$(docker run --rm $TEST_IMAGE uname) 95 | assert_equals "$out" "Linux" 96 | } 97 | 98 | function test_docker_mount { 99 | log_info "Testing mounting a folder with Alpine Linux image" 100 | local readonly out=$(docker run --rm -v $(pwd):/src gliderlabs/alpine:3.2 /bin/sh -c "cat /src/$TEST_FOLDER/$TEST_FILE") 101 | assert_equals "$out" "$TEST_FILE_CONTENTS" 102 | } 103 | 104 | cleanup 105 | create_machine 106 | start_machine 107 | test_setup 108 | create_test_project 109 | test_docker_osx_dev 110 | test_docker_run 111 | test_docker_mount 112 | -------------------------------------------------------------------------------- /test/resources/docker-compose-base.yml: -------------------------------------------------------------------------------- 1 | web: 2 | image: foo/bar 3 | ports: 4 | - "3000:3000" 5 | volumes: 6 | - /host:/guest 7 | db: 8 | image: baz/blah 9 | -------------------------------------------------------------------------------- /test/resources/docker-compose-extends.yml: -------------------------------------------------------------------------------- 1 | web: 2 | extends: 3 | file: docker-compose-base.yml 4 | service: web 5 | volumes: 6 | - /host2:/guest2 7 | links: 8 | - db 9 | db: 10 | extends: 11 | file: docker-compose-base.yml 12 | service: db 13 | -------------------------------------------------------------------------------- /test/resources/docker-compose-multiple-containers-with-volumes.yml: -------------------------------------------------------------------------------- 1 | web: 2 | image: foo/bar 3 | ports: 4 | - "3000:3000" 5 | volumes: 6 | - /host1:/guest1 7 | - /host2:/guest2 8 | links: 9 | - db 10 | db: 11 | image: baz/blah 12 | volumes: 13 | - /foo/bar:/abc/def 14 | cache: 15 | volumes: 16 | - /:/ 17 | image: abc/def 18 | -------------------------------------------------------------------------------- /test/resources/docker-compose-multiple-volumes.yml: -------------------------------------------------------------------------------- 1 | web: 2 | image: foo/bar 3 | ports: 4 | - "3000:3000" 5 | volumes: 6 | - /host1:/guest1 7 | - /foo/bar/baz:/foo/bar/baz 8 | - /source/path:/guest/path 9 | links: 10 | - db 11 | db: 12 | image: baz/blah -------------------------------------------------------------------------------- /test/resources/docker-compose-named-volumes.yml: -------------------------------------------------------------------------------- 1 | web: 2 | image: foo/bar 3 | ports: 4 | - "3000:3000" 5 | volumes: 6 | - /host:/guest 7 | - named_volume:/guest2 8 | links: 9 | - db 10 | db: 11 | image: baz/blah 12 | -------------------------------------------------------------------------------- /test/resources/docker-compose-no-volumes.yml: -------------------------------------------------------------------------------- 1 | web: 2 | image: foo/bar 3 | ports: 4 | - "3000:3000" 5 | links: 6 | - db 7 | db: 8 | image: baz/blah -------------------------------------------------------------------------------- /test/resources/docker-compose-non-mounted-volumes.yml: -------------------------------------------------------------------------------- 1 | web: 2 | image: foo/bar 3 | ports: 4 | - "3000:3000" 5 | volumes: 6 | - /host:/guest 7 | - abc 8 | - def 9 | - /a:/b 10 | links: 11 | - db 12 | db: 13 | image: baz/blah -------------------------------------------------------------------------------- /test/resources/docker-compose-one-volume-access-modifier.yml: -------------------------------------------------------------------------------- 1 | web: 2 | image: foo/bar 3 | ports: 4 | - "3000:3000" 5 | volumes: 6 | - /host:/guest:ro 7 | links: 8 | - db 9 | db: 10 | image: baz/blah 11 | -------------------------------------------------------------------------------- /test/resources/docker-compose-one-volume-double-quotes.yml: -------------------------------------------------------------------------------- 1 | web: 2 | image: foo/bar 3 | ports: 4 | - "3000:3000" 5 | volumes: 6 | - "/host:/guest" 7 | links: 8 | - db 9 | db: 10 | image: baz/blah 11 | -------------------------------------------------------------------------------- /test/resources/docker-compose-one-volume-single-quotes.yml: -------------------------------------------------------------------------------- 1 | web: 2 | image: foo/bar 3 | ports: 4 | - "3000:3000" 5 | volumes: 6 | - '/host:/guest' 7 | links: 8 | - db 9 | db: 10 | image: baz/blah 11 | -------------------------------------------------------------------------------- /test/resources/docker-compose-one-volume.yml: -------------------------------------------------------------------------------- 1 | web: 2 | image: foo/bar 3 | ports: 4 | - "3000:3000" 5 | volumes: 6 | - /host:/guest 7 | links: 8 | - db 9 | db: 10 | image: baz/blah -------------------------------------------------------------------------------- /test/resources/ignore-file-empty.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brikis98/docker-osx-dev/8d16d03ceb2866ed1284ea57dc94d0003df6ed8c/test/resources/ignore-file-empty.txt -------------------------------------------------------------------------------- /test/resources/ignore-file-with-comments.txt: -------------------------------------------------------------------------------- 1 | # Should be ignored 2 | foo 3 | bar 4 | # Should also be ignored 5 | baz 6 | -------------------------------------------------------------------------------- /test/resources/ignore-file-with-includes.txt: -------------------------------------------------------------------------------- 1 | # Should be ignored 2 | foo 3 | bar 4 | # Should also be ignored 5 | baz 6 | !bar 7 | # Should also be included 8 | !foo 9 | -------------------------------------------------------------------------------- /test/resources/ignore-file-with-multiple-entries.txt: -------------------------------------------------------------------------------- 1 | foo 2 | bar 3 | baz 4 | -------------------------------------------------------------------------------- /test/resources/ignore-file-with-one-entry.txt: -------------------------------------------------------------------------------- 1 | foo 2 | -------------------------------------------------------------------------------- /test/test_helper.bash: -------------------------------------------------------------------------------- 1 | flunk() { 2 | { if [ "$#" -eq 0 ]; then cat - 3 | else echo "$@" 4 | fi 5 | } >&2 6 | return 1 7 | } 8 | 9 | assert_success() { 10 | if [ "$status" -ne 0 ]; then 11 | flunk "command failed with exit status $status" 12 | elif [ "$#" -gt 0 ]; then 13 | assert_output "$1" 14 | fi 15 | } 16 | 17 | assert_failure() { 18 | if [ "$status" -eq 0 ]; then 19 | flunk "expected failed exit status" 20 | elif [ "$#" -gt 0 ]; then 21 | assert_output "$1" 22 | fi 23 | } 24 | 25 | assert_equal() { 26 | if [ "$1" != "$2" ]; then 27 | { echo "expected: $1" 28 | echo "actual: $2" 29 | } | flunk 30 | fi 31 | } 32 | 33 | assert_output() { 34 | local expected 35 | if [ $# -eq 0 ]; then expected="$(cat -)" 36 | else expected="$1" 37 | fi 38 | assert_equal "$expected" "$output" 39 | } 40 | 41 | assert_line() { 42 | if [ "$1" -ge 0 ] 2>/dev/null; then 43 | assert_equal "$2" "${lines[$1]}" 44 | else 45 | local line 46 | for line in "${lines[@]}"; do 47 | if [ "$line" = "$1" ]; then return 0; fi 48 | done 49 | flunk "expected line \`$1'" 50 | fi 51 | } 52 | 53 | refute_line() { 54 | if [ "$1" -ge 0 ] 2>/dev/null; then 55 | local num_lines="${#lines[@]}" 56 | if [ "$1" -lt "$num_lines" ]; then 57 | flunk "output has $num_lines lines" 58 | fi 59 | else 60 | local line 61 | for line in "${lines[@]}"; do 62 | if [ "$line" = "$1" ]; then 63 | flunk "expected to not find line \`$line'" 64 | fi 65 | done 66 | fi 67 | } 68 | 69 | assert() { 70 | if ! "$@"; then 71 | flunk "failed: $@" 72 | fi 73 | } 74 | 75 | stub() { 76 | [ -d "$BATS_TEST_DIRNAME/stub" ] || mkdir "$BATS_TEST_DIRNAME/stub" 77 | #touch "$BATS_TEST_DIRNAME/stub/$1" 78 | echo "$2" > "$BATS_TEST_DIRNAME/stub/$1" 79 | chmod +x "$BATS_TEST_DIRNAME/stub/$1" 80 | } 81 | 82 | rm_stubs() { 83 | rm -rf "$BATS_TEST_DIRNAME/stub" 84 | } 85 | --------------------------------------------------------------------------------