├── .dock ├── .dockerignore ├── .travis.yml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── bin ├── dock └── test ├── doc └── img │ └── docker-for-mac-file-sharing.png ├── script ├── entrypoint.bash └── install-dock └── test ├── configuration ├── build_arg.bats ├── build_context.bats ├── build_flags.bats ├── container_hostname.bats ├── container_name.bats ├── dockerfile.bats ├── env_var.bats ├── image.bats ├── optional_env_var.bats ├── privileged.bats ├── publish.bats ├── required_env_var.bats ├── run_flags.bats ├── volume.bats └── workspace_path.bats ├── environment └── dock_force_destroy.bash ├── helpers ├── ask.bats ├── container_hostname.bats ├── container_name.bats ├── group_id.bats ├── interactive.bats ├── linux.bats ├── mac_os.bats ├── repo_path.bats ├── user_id.bats └── workspace_path.bats ├── options ├── attach.bats ├── config.bats ├── detach.bats ├── extend.bats ├── force.bats ├── help.bats ├── quiet.bats ├── verbose_version.bats └── version.bats └── utils.bash /.dock: -------------------------------------------------------------------------------- 1 | # Loaded by Dock when any `dock` command is executed in this repository. 2 | 3 | # Create development container using image built from this Dockerfile 4 | dockerfile Dockerfile 5 | 6 | # Allow Dock containers to be created within this Dock container, since we 7 | # explicitly want to test that! 8 | dock_in_dock true 9 | 10 | # Running a Docker daemon inside the container requires additional privileges 11 | privileged true 12 | 13 | # Execute this script with the original command arguments passed to it 14 | entrypoint script/entrypoint.bash 15 | 16 | # Ensure all the temporary files we create during test runs use the host file 17 | # system (for performance) and are destroyed between runs. 18 | volume "/tmp" 19 | 20 | # Preserve Docker layer cache between runs 21 | volume "$(container_name)_docker:/var/lib/docker" 22 | 23 | default_command bash 24 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Exclude all files from the Docker build context by default. 2 | # This speeds up the start time of any `dock` command. 3 | # 4 | # Whitelist files/patterns for inclusion by prepending "!" to them. 5 | * 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | services: 4 | - docker 5 | 6 | script: 7 | - bin/test 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Dock Change Log 2 | 3 | ## 1.4.8 (2018-03-22) 4 | 5 | * Add WORKSPACE_DIR as environment variable 6 | * Fix attachment flow when running `dock` with active project container 7 | 8 | ## 1.4.7 (2017-08-24) 9 | 10 | * Fix port_taken_on_localhost method for OSX machines 11 | 12 | ## 1.4.6 (2017-05-11) 13 | 14 | * Update README with startup_services configuration allowing users to specify 15 | which project services to start up when being integrated into a multi-project Dock 16 | environment. 17 | 18 | ## 1.4.5 (2017-04-11) 19 | 20 | * Modify default container entry command in order to prevent Dock containers 21 | from exiting following project extensions 22 | 23 | ## 1.4.4 (2017-04-10) 24 | 25 | * Set project repo root to current working directory rather than top level 26 | git repository root to allow dock to run in git repo subdirs 27 | * Include list of projects currently a part of a dock container within dock 28 | extension messaging 29 | 30 | ## 1.4.3 (2017-03-29) 31 | 32 | * Change defaults for privileged and pull_latest flags from false to true 33 | * Add startup_services configuration option allowing projects to specify 34 | which services and variants therein to launch during terraforming of an 35 | extended container 36 | * Modify extension option to only record compose construction label if 37 | docker-compose file exists in project repo 38 | * Add image check to extension option to allow base projects to specify 39 | an image to utilize for Dock development container 40 | * Apply various refactorizations 41 | 42 | ## 1.4.2 (2017-03-22) 43 | 44 | * Hotfix removing docker-compose file verification during extension. 45 | Technically, functionality should not have been affected with the 46 | verification in place though it makes sense to remove it. 47 | 48 | ## 1.4.1 (2017-03-13) 49 | 50 | * Add 'transform' option allowing users to fully merge and compose an existing 51 | extended Dock container 52 | * Download and install docker-compose tool within Dock Dockerfile 53 | * Replace Dock configuration label (i.e. compose/dock.) values with the path 54 | for each file within the container rather than its contents 55 | 56 | ## 1.4.0 (2017-02-27) 57 | 58 | * Add 'extension' option allowing users to build 59 | environments consisting of multiple projects/services 60 | * Update container inspection and modification methods to operate 61 | on either the project default dock container or a different 62 | container targeted by the user (as a means of maintaining backwards 63 | compatibility) 64 | * Refactor basic dock run args compilation logic into a 65 | separate reusable method 66 | 67 | ## 1.3.1 (2016-11-24) 68 | 69 | * Fix `integer expression expected` warning on hosts running Bash 4 or newer 70 | 71 | ## 1.3.0 (2016-11-23) 72 | 73 | * Add `build_context` configuration option allowing the build context 74 | path/URL to be specified 75 | * Add support for automatically destroying already-existing/running Dock 76 | containers for a project by specifying the `DOCK_FORCE_DESTROY` environment 77 | variable (useful in CI environments where containers can get left behind) 78 | 79 | ## 1.2.0 (2016-11-15) 80 | 81 | * Don't set container hostname by default 82 | * Rename `hostname` option to `container_hostname` so it doesn't conflict with 83 | `hostname` executable 84 | 85 | ## 1.1.0 (2016-11-09) 86 | 87 | * Add `build_flags` configuration option allowing you to specify additional 88 | arguments to include in the `docker build ...` command 89 | * Rename `run_args` to `run_flags` so naming convention better matches the 90 | `build_flags` configuration option 91 | * Fix symlink resolution of `dock` executable to not require GNU version of 92 | `readlink` 93 | 94 | ## 1.0.0 (2016-10-31) 95 | 96 | * Initial release 97 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM centos:7.2.1511 2 | 3 | RUN yum -y install \ 4 | git \ 5 | # Docker daemon won't start without `iptables` installed 6 | iptables-services \ 7 | # Used for a variety of simple tests for port forwarding/publishing 8 | nmap-ncat net-tools \ 9 | # Allow `dock-user` to escalate privileges if necessary 10 | sudo \ 11 | # Allow us to run using OverlayFS file system 12 | yum-plugin-ovl \ 13 | && yum clean all 14 | 15 | # Install Docker daemon so we can run Docker inside the Dock container 16 | RUN curl -L https://get.docker.com/builds/Linux/x86_64/docker-1.12.3.tgz \ 17 | | tar -xzf - -C /usr/bin --strip-components=1 18 | 19 | # Install docker-compose so we can run the docker-compose tool inside the Dock container 20 | RUN curl -L https://github.com/docker/compose/releases/download/1.11.2/run.sh > /usr/local/bin/docker-compose \ 21 | && chmod +x /usr/local/bin/docker-compose 22 | 23 | # Install Bats testing framework 24 | RUN mkdir -p /src \ 25 | && git clone --depth=1 https://github.com/sstephenson/bats.git /src/bats \ 26 | && cd /src/bats \ 27 | && ./install.sh /usr/local \ 28 | && rm -rf /src 29 | 30 | # Create a non-root user with sudo privileges 31 | RUN useradd dock-user \ 32 | && echo "%dock-user ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/dock-user \ 33 | && groupadd --non-unique -g $(grep dock-user /etc/group | cut -d: -f3) docker 34 | USER dock-user 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2016 Brigade Group, Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/brigade/dock.svg?branch=master)](https://travis-ci.org/brigade/dock) 2 | 3 | # Dock 4 | 5 | `dock` is a tool for defining, building, and running self-contained development 6 | environments inside [Docker](https://www.docker.com/) containers. 7 | 8 | * [Installation](#installation) 9 | * [Getting Started](#getting-started) 10 | * [Usage](#usage) 11 | * [Attach to an already-running container](#attach-to-an-already-running-container) 12 | * [Destroy an already-running container](#destroy-an-already-running-container) 13 | * [Run a container in the background](#run-a-container-in-the-background) 14 | * [Extend an existing container](#extend-an-existing-container) 15 | * [Terraform an existing container](#terraform-an-existing-container) 16 | * [Automatically execute a script in a Dock container](#automatically-execute-a-script-in-a-dock-container) 17 | * [Expose services inside the container on your host](#expose-services-inside-the-container-on-your-host) 18 | * [Configuration](#configuration) 19 | * [Options](#options) 20 | * [Helpers](#helpers) 21 | * [Development](#development) 22 | * [Running Tests](#running-tests) 23 | * [Change Log](#change-log) 24 | * [License](#license) 25 | 26 | ## Installation 27 | 28 | All you need to run `dock` is to have Docker and Bash installed on your system. 29 | 30 | ### via Homebrew (macOS) 31 | 32 | ```bash 33 | brew tap brigade/dock 34 | brew install dock 35 | ``` 36 | 37 | You can then upgrade at any time by running: 38 | 39 | ``` 40 | brew upgrade dock 41 | ``` 42 | 43 | ### via Bash (Linux/macOS) 44 | 45 | You can install/upgrade Dock with this command: 46 | 47 | ```bash 48 | curl https://raw.githubusercontent.com/brigade/dock/master/script/install-dock | bash 49 | ``` 50 | 51 | It will ask for your sudo password only if necessary. 52 | 53 | ### Docker for Mac 54 | 55 | If you are using [Docker for Mac](https://docs.docker.com/docker-for-mac/), 56 | you need to add `/usr/local/Cellar` (if you installed Dock using Homebrew) or 57 | `/usr/local/bin` (if you installed using the Bash script) to the list of mountable 58 | directories. 59 | 60 | This allows Dock to mount itself within the container so you can recursively 61 | execute Dock within Dock containers. You can add the path via 62 | **Preferences** -> **File Sharing**: 63 | 64 |

65 | Docker for Mac File Sharing 66 |

67 | 68 | ## Getting Started 69 | 70 | You can try out Dock against the Dock repository itself to get a feel for how 71 | it works. 72 | 73 | **WARNING**: This will start a privileged container on your machine (in order to 74 | start a Docker daemon within the container, it needs extended privileges). 75 | Proceed at your own risk. 76 | 77 | ```bash 78 | git clone git://github.com/brigade/dock 79 | cd dock 80 | bin/dock # If you have installed Dock then you can just run `dock` 81 | ``` 82 | 83 | After running `dock` inside the Dock repository, you should be running inside 84 | a Docker container. The environment will look and feel like CentOS 7 because 85 | it is based off of that image (see the corresponding [Dockerfile](Dockerfile)). 86 | Your current directory will be `/workspace` inside that container, and the 87 | contents of that directory will be the Dock repository itself (i.e. the current 88 | project). 89 | 90 | ``` 91 | $ pwd 92 | /workspace 93 | $ ls 94 | Dockerfile LICENSE.md README.md ... 95 | ``` 96 | 97 | Any changes you make to these files will automatically be reflected in the 98 | original repository, and vice versa. This allows you to continue using your 99 | favorite tools and editors to make changes to your project, but actually 100 | run code or tests _inside_ the container to isolate these from the rest of 101 | your system. 102 | 103 | ## Usage 104 | 105 | Run `dock` within your repository by specifying any options followed by the 106 | command you wish to run in the Dock container: 107 | 108 | ``` 109 | cd path/to/repo 110 | dock [options] [command...] 111 | ``` 112 | 113 | If no command is given, Dock will execute the `default_command` defined in your 114 | Dock configuration file. If no `default_command` is specified, the 115 | `ENTRYPOINT`/`CMD` directives defined in the Dockerfile that created the image 116 | Dock is running will dictate which command will be executed. 117 | 118 | Option | Description 119 | -----------------------|------------------------------------------------------- 120 | `-a` | Attach to an already-running Dock container 121 | `-c config-file` | Path of Dock configuration file to use (default `.dock`) 122 | `-d` | Detach/daemonize (run resulting Dock container in the background) 123 | `-e dock-id` | Extend an existing Dock container (add new project configuration and services) 124 | `-f` | Force creation of new container (destroying old one if it exists) 125 | `-h` | Display summary of command line options 126 | `-q` | Silence Dock-related output (so only output from command run in the container is shown) 127 | `-t dock-id` | Terraform an existing Dock container (fully compose and merge embedded projects) 128 | `-v` | Display version information 129 | `-V` | Display verbose version information (for bug reports) 130 | 131 | ### Attach to an already-running container 132 | 133 | Dock is intended to make it easy to manage a _single_ development environment 134 | for a repository, like you would any virtual machine. Thus it *explicitly 135 | prevents* you from running `dock` multiple times in the same project to run 136 | multiple containers for that single project (if you must, you can get around 137 | this by cloning the project into separate directories with different names so 138 | they are assigned different container names by default). 139 | 140 | You can however open multiple shells to the same container by attaching to an 141 | already-running Dock container by running `dock -a` in the project directory. 142 | 143 | ### Destroy an already-running container 144 | 145 | If you run `dock` in a repository which already has an associated container 146 | running, you'll prompted to confirm if you would like to destroy the current 147 | container (if there's no interactive shell, Dock will halt with an error). 148 | 149 | You can bypass this prompt by setting the environment variable 150 | `DOCK_FORCE_DESTROY=1`. This is useful in CI environments where there is no 151 | interactive shell but you are sure you always want to destroy any lingering 152 | containers. 153 | 154 | ### Run a container in the background 155 | 156 | If you want to have Dock start up isolated services and don't require an active 157 | shell session inside the container, you can start the container in detached 158 | mode. 159 | 160 | Run `dock -d` or specify `detach true` in your Dock configuration. 161 | 162 | ### Extend an existing container 163 | 164 | You can run multiple projects within a single `Dock` container in very much the 165 | same way `Dock` runs a single project. 166 | 167 | If you run `dock` in a repository and specify a `Dock` container identifier (used 168 | to annotate and distinguish between `Dock` container environments) to extend, `dock` 169 | will add the project src and all associated environment configuration (e.g. volumes, 170 | exposed ports, environment variables) into the `Dock` container represented by the 171 | identifier. 172 | 173 | If the specified `Dock` container does not exist, `dock` will create and run a new 174 | `Dock` container based on the configuration of the current project. 175 | 176 | ```bash 177 | 178 | $ cd / # step 1 179 | $ dock -e # step 2 180 | ... 181 | SUCCESS: Dock successfully created! 182 | $ cd / # step 3 183 | $ dock -e # step 4 184 | ... 185 | SUCCESS: Dock successfully extended! 186 | ``` 187 | 188 | ### Terraform an existing container 189 | 190 | Specifying a `-t` flag along with a `Dock` container identifier when running `Dock` 191 | will result in the full composition and merging of the embedded project components 192 | within the container. This option essentially stands up all services and associated 193 | components added to the target `Dock` container using the `extends` options while also 194 | handling the proper identification and deduplication of shared components. 195 | 196 | The result of the operation will be a container consistent with running `docker-compose up` 197 | on each docker-compose.yml file associated with all projects embedded within 198 | and obeying [Docker's official docker-compose extension support](https://docs.docker.com/compose/extends/). 199 | ```bash 200 | 201 | $ dock -t # step 1 202 | ... 203 | SUCCESS: Dock successfully terraformed! 204 | ``` 205 | 206 | ### Automatically execute a script in a Dock container 207 | 208 | By adding a correct [shebang line](https://en.wikipedia.org/wiki/Shebang_(Unix)) 209 | to your script, you can have the script automatically run inside a Dock container. 210 | 211 | ```bash 212 | #!/usr/local/bin/dock bash 213 | echo "We are in a container!" 214 | ``` 215 | 216 | If you run this in a project with a valid Dock configuration file, the script 217 | will invoke Dock which will start a container using the image defined by the 218 | configuration execute the script using whatever command you passed as the second 219 | argument (`bash` in this case). 220 | 221 | Any Dock-related output will go to the standard error stream, but the standard 222 | output stream will contain output from the original script. If you need to 223 | inspect the command's standard error stream and don't want to deal with filtering 224 | out Dock-related output, you can specify the `QUIET` environment variable in order 225 | to silence all Dock-related output. Note that this can potentially be confusing 226 | since if the image has never been built before it may take a while to build, 227 | giving the appearance of nothing happening. 228 | 229 | **WARNING**: Never specify more than one argument to the shebang line. Different 230 | operating systems have different restrictions on shebangs. While some allow you 231 | to specify as many as you want, Linux in particular will treat all arguments 232 | after the executable as a single argument. For example, the following code: 233 | 234 | ```bash 235 | #!/usr/local/bin/dock bash -c 'some command' 236 | ... 237 | ``` 238 | 239 | ...will treat the shebang line as `/usr/local/bin/dock "bash -c 'some command'"` 240 | (i.e. treating `bash -c 'some command'` as a single argument), which will fail 241 | since there is no such file. 242 | 243 | If you need to execute a script with a complicated set of arguments, create 244 | a wrapper script: 245 | 246 | ```bash 247 | #!/usr/local/bin/dock script/my-wrapper-script 248 | ... 249 | ``` 250 | 251 | The wrapper script will be passed the path to the script file as a single 252 | argument. It is up to you to write the script to know how to handle/execute 253 | the file it is given. 254 | 255 | ### Expose services inside the container on your host 256 | 257 | While an important feature offered by Dock is isolating your development 258 | environments from each other (e.g. so that services listening on the same 259 | port don't conflict), it is convenient to be able to expose these ports on 260 | your host so you can easily interact with them using tools installed on 261 | your host. 262 | 263 | You can expose/publish a port using the `publish` command in your 264 | configuration: 265 | 266 | ```bash 267 | publish 3306:3306 # Expose MySQL on the same port on the host 268 | ``` 269 | 270 | Where this differs from the `--publish` flag of `docker run` is that if the 271 | port is already taken by another process on your machine (e.g. another Dock 272 | container for a different project) you'll still be able to start the 273 | container. You'll see a warning letting you know that the port could not 274 | be exposed. 275 | 276 | If you really need to expose the service for a given project, stop the 277 | container for the other project so it releases the port, start the project 278 | whose service you want to expose, and then start back up your other project. 279 | 280 | Alternatively, you can decide to expose the services to different ports on 281 | the host so they don't conflict. 282 | 283 | ## Configuration 284 | 285 | The Dock configuration file is quite flexible since it's simply sourced as a 286 | Bash script. By default Dock looks for a `.dock` file at the root of your 287 | repository and sources it. 288 | 289 | This means anything you can do in Bash you can also do in this script. 290 | Therefore caution is required! 291 | 292 | However, this also provides an incredible amount of power. A common use case 293 | is to dynamically change which environment variables are set or volumes are 294 | mounted based on whether you are running in a CI (continuous integration) 295 | testing environment or just a regular development environment. 296 | 297 | Dock exposes configuration options and helpers. 298 | 299 | ### Options 300 | 301 | These configuration options can be set in your Dock configuration file. 302 | 303 | * [`attach_command`](#attach_command) 304 | * [`build_arg`](#build_arg) 305 | * [`build_context`](#build_context) 306 | * [`build_flags`](#build_flags) 307 | * [`container_hostname`](#container_hostname) 308 | * [`container_name`](#container_name) 309 | * [`default_command`](#default_command) 310 | * [`detach`](#detach) 311 | * [`detach_keys`](#detach_keys) 312 | * [`dockerfile`](#dockerfile) 313 | * [`dock_in_dock`](#dock_in_dock) 314 | * [`env_var`](#env_var) 315 | * [`image`](#image) 316 | * [`optional_env_var`](#optional_env_var) 317 | * [`privileged`](#privileged) 318 | * [`publish`](#publish) 319 | * [`pull_latest`](#pull_latest) 320 | * [`required_env_var`](#required_env_var) 321 | * [`run_flags`](#run_flags) 322 | * [`startup_services`](#startup_services) 323 | * [`volume`](#volume) 324 | * [`workspace_path`](#workspace_path) 325 | 326 | #### `attach_command` 327 | 328 | Command to execute when attaching to a container via `dock -a`. By default this 329 | is just `bash`. 330 | 331 | ```bash 332 | attach_command gosu app_user 333 | ``` 334 | 335 | **WARNING**: You must split the command into individual arguments in order for 336 | them to be executed correctly. This means if you have a single argument with 337 | whitespace you'll need to wrap it in quotes or escape it: 338 | 339 | ```bash 340 | attach_command echo "This is a single argument" 341 | attach_command echo These are multiple separate arguments 342 | ``` 343 | 344 | #### `build_arg` 345 | 346 | Specify an additional build argument to pass to the `docker build` command that 347 | Dock will execute when building the Dockerfile specified by the `dockerfile` 348 | option. Ignored if you have only specified an `image` rather than a `dockerfile`, 349 | as no building is done in such case. 350 | 351 | ```bash 352 | build_arg MY_ARG "some value" 353 | ``` 354 | 355 | #### `build_context` 356 | 357 | Specify the path or URL to use for the build context of the `docker build` 358 | command that Dock will execute when building the Dockerfile. Ignored if you 359 | have only specified an `image` rather than a `dockerfile`. 360 | 361 | ```bash 362 | build_context path/in/repo 363 | 364 | build_context https://example.com/context.tar.gz 365 | ``` 366 | 367 | See the [`docker build`](https://docs.docker.com/engine/reference/commandline/build) 368 | documentation for details on how the build context and Dockerfile paths are handled. 369 | 370 | #### `build_flags` 371 | 372 | Specify additional arguments for the `docker build` command that Dock will 373 | execute when building the Dockerfile specified by the `dockerfile` option. 374 | Ignored if you have only specified an `image` rather than a `dockerfile`, as 375 | no building is done in such case. 376 | 377 | **Note**: This is not exclusively for the `--build-arg` flag that allows you to 378 | specify build-time variables (use the `build_arg` option instead). It allows you 379 | to specify _any_ flag that `docker build` accepts, e.g. `--memory`, `--no-cache`, 380 | etc. 381 | 382 | ```bash 383 | build_flags --no-cache 384 | ``` 385 | 386 | #### `container_hostname` 387 | 388 | Specifies an explicit hostname for the container. 389 | 390 | ```bash 391 | container_hostname "$(container_name).test.com" 392 | ``` 393 | 394 | #### `container_name` 395 | 396 | Specifies the name to give the Dock container. Default is the directory name of 397 | the repository followed by "-dock", e.g. `my-project-dock`. 398 | 399 | The container name is what Dock uses to determine if the container is already 400 | running. 401 | 402 | ```bash 403 | container_name my-container-name 404 | ``` 405 | 406 | #### `default_command` 407 | 408 | Specifies a default command to run if no command is explicitly given. 409 | 410 | You should ideally always have a default command specified so that a user who 411 | knows nothing about your repository can simply run `dock` to get started. 412 | 413 | ```bash 414 | default_command script/start-services 415 | ``` 416 | 417 | **WARNING**: You must split the command into individual arguments in order for 418 | them to be executed correctly. This means if you have a single argument with 419 | whitespace you'll need to wrap it in quotes or escape it: 420 | 421 | ```bash 422 | default_command echo "This is a single argument" 423 | default_command echo These are multiple separate arguments 424 | ``` 425 | 426 | #### `detach` 427 | 428 | Specifies whether the Dock container should run in the background (e.g. the 429 | `--detach` flag of `docker run`). 430 | 431 | You can re-attach to a detached Dock container by running `dock -a`. 432 | 433 | ```bash 434 | detach true 435 | ``` 436 | 437 | #### `detach_keys` 438 | 439 | Specifies the key sequence to detach from the container. 440 | 441 | Dock changes this sequence from the default `ctrl-p,p` to `ctrl-x,x`. This 442 | makes the `ctrl-p` readline shortcut for cycling through previous commands 443 | work as expected without needing to type `ctrl-p` twice, which is useful 444 | when you are running a shell in the container. 445 | 446 | This is almost certainly what you want, but `detach_keys` is available if you 447 | want to change it to some other sequence. 448 | 449 | #### `dockerfile` 450 | 451 | Specify path to the Dockerfile to build into an image used by the Dock 452 | container. If a relative path is given, this will be relative to the 453 | repository root directory (even if you have specified a custom 454 | [`build_context`](#build_context), the path is always relative to the root 455 | of the repository). 456 | 457 | Cannot specify `dockerfile` and `image` options in the same configuration. 458 | 459 | ```bash 460 | dockerfile Dockerfile 461 | ``` 462 | 463 | #### `dock_in_dock` 464 | 465 | Specify whether to allow Dock to create containers within a Dock container. 466 | Default is `false`, and this is almost always what you want. 467 | 468 | The Dock project itself needs to test the creation of Dock containers within 469 | a Dock container, so it enables this feature. 470 | 471 | ```bash 472 | dock_in_dock true 473 | ``` 474 | 475 | #### `env_var` 476 | 477 | Specifies an environment variable name and value to be set inside the 478 | container. 479 | 480 | ```bash 481 | env_var MY_ENV_VAR "my value" 482 | ``` 483 | 484 | #### `image` 485 | 486 | If `dockerfile` is specified, defines the image name to tag the build with. 487 | 488 | Otherwise this specifies the image used to create the container. 489 | 490 | ```bash 491 | image centos:7.2.1511 492 | ``` 493 | 494 | #### `optional_env_var` 495 | 496 | Specifies the name of an environment variable which is included in the 497 | container environment if defined in the context in which `dock` is run. 498 | If not defined, it is ignored and no value is set inside the container. 499 | 500 | This is a way of safely declaring which environment variables you want 501 | to "inject" from your host into the Dock container. 502 | 503 | ```bash 504 | optional_env_var MY_OPTIONAL_ENV_VAR 505 | ``` 506 | 507 | #### `privileged` 508 | 509 | Whether to run the container with elevated privileges. Defaults to `false`. 510 | 511 | ```bash 512 | privileged true 513 | ``` 514 | 515 | #### `publish` 516 | 517 | Expose a port inside the Dock container to the host. Useful if you want to 518 | make it easier for developers to connect to services inside the Dock container 519 | using tools installed on their host machine. 520 | 521 | If another process on the host has already bound to the port, Dock will display 522 | a warning but will otherwise ignore the error, since you will still be able to 523 | access the service from inside the container. 524 | 525 | The format of the port specification is the same as the `-p`/`--publish` flag 526 | of the `docker run` command (`[host-ip:][host-port:]container-port`). 527 | 528 | ```bash 529 | # Expose MySQL 530 | publish "3306:3306" 531 | ``` 532 | 533 | #### `pull_latest` 534 | 535 | Specifies whether Docker should always attempt to pull the latest version of 536 | an image/tag to ensure it is up to date. Default is `false`. 537 | 538 | ```bash 539 | pull_latest true 540 | ``` 541 | 542 | #### `required_env_var` 543 | 544 | Specifies the name of an environment variable that must be defined. 545 | If not defined, Dock will halt with an error message. 546 | 547 | ```bash 548 | required_env_var MY_REQUIRED_ENV_VAR 549 | ``` 550 | 551 | #### `run_flags` 552 | 553 | Specify additional arguments for the `docker run` command that Dock will 554 | execute. 555 | 556 | Most common flags are configured via the various options allowed in the Dock 557 | configuration, e.g. `env_var`, `volume`, etc. However, for special cases 558 | this is provided to allow you to specify additional flags. 559 | 560 | ```bash 561 | run_flags --memory 1g 562 | ``` 563 | 564 | #### `startup_services` 565 | 566 | Services to startup when terraforming a multi-project, integrated and self-contained Dock development environment, as defined within a project's docker-compose.yml. 567 | 568 | ```bash 569 | startup_services "my_service mysql" 570 | ``` 571 | 572 | #### `volume` 573 | 574 | Specify a volume or bind mount. 575 | 576 | ```bash 577 | # Mount /var/lib/docker directory in the container on the host file system. 578 | # Destroyed on container shutdown. Useful if you are using a container file 579 | # system like OverlayFS and want to perform a lot of writes with the 580 | # performance of the underlying host file system. 581 | volume "/var/lib/docker" 582 | 583 | # Create a data volume named "${container_name}_docker" which store the 584 | # contents of the container's /var/lib/docker directory. Will not be destroyed 585 | # when the container is shutdown/destroyed. 586 | volume "${container_name}_docker:/var/lib/docker" 587 | 588 | # Mount a file/directory on the host file system at a particular location in 589 | # the container. Use the ${repo_root} variable so you reference a path that 590 | # will exist regardless of the location of your repository. 591 | volume "${repo_root}/script/my-script:/usr/bin/my-script" 592 | ``` 593 | 594 | #### `workspace_path` 595 | 596 | Define the path in the container that the repository will be mounted at. 597 | Default is `/workspace`. 598 | 599 | ```bash 600 | workspace_path /my-custom-dir 601 | ``` 602 | 603 | ### Helpers 604 | 605 | Dock defines a number of helper functions which may be useful when writing your 606 | configuration file. 607 | 608 | **WARNING**: Bash variables and functions are referenced differently. Since 609 | these helpers are all functions, always use `$(...)` to include the output of a 610 | function in a string, e.g.: 611 | 612 | ```bash 613 | volume "$(container_name)_docker:/var/lib/docker" 614 | ``` 615 | 616 | * [`ask`](#ask) 617 | * [`detach`](#detach) 618 | * [`group_id`](#group_id) 619 | * [`container_hostname`](#container_hostname) 620 | * [`interactive`](#interactive) 621 | * [`linux`](#linux) 622 | * [`mac_os`](#mac_os) 623 | * [`privileged`](#privileged) 624 | * [`repo_path`](#repo_path) 625 | * [`user_id`](#user_id) 626 | * [`workspace_path`](#workspace_path) 627 | 628 | #### `ask` 629 | 630 | Asks a user for input. Uses the default answer if we're not running in 631 | an interactive context (as specified by `interactive`). 632 | 633 | All arguments must be specified. 634 | 635 | ```bash 636 | ask "Are you sure? (y/n)" n variable_to_store_answer_in 637 | ``` 638 | 639 | #### `container_hostname` 640 | 641 | Outputs the explicit hostname that will be assigned to the container, if one 642 | was specified via `container_hostname "some.name"`. Otherwise it returns 643 | a non-zero exit status. 644 | 645 | #### `container_name` 646 | 647 | Outputs the name of the Docker container that Dock will create. 648 | 649 | #### `detach` 650 | 651 | Returns whether container will be detached on startup. 652 | 653 | Returns zero exit status (success) if yes. 654 | 655 | #### `group_id` 656 | 657 | Group ID of the user that ran the Dock command. 658 | 659 | #### `interactive` 660 | 661 | Whether we're running Dock in an interactive context where a human 662 | can provide input (e.g. standard input is a TTY). 663 | 664 | Returns zero exit status (success) if yes. 665 | 666 | #### `linux` 667 | 668 | Whether the host is running Linux. 669 | 670 | Returns zero exit status (success) if yes. 671 | 672 | #### `mac_os` 673 | 674 | Whether the host is running macOS. 675 | 676 | Returns zero exit status (success) if yes. 677 | 678 | #### `privileged` 679 | 680 | Returns whether the container will be started with extended privileges. 681 | 682 | Returns zero exit status (success) if yes. 683 | 684 | #### `repo_path` 685 | 686 | Path to repository root on the host system. 687 | 688 | #### `user_id` 689 | 690 | User ID of the user that ran the Dock command. 691 | 692 | #### `workspace_path` 693 | 694 | Directory in the container that the repository will be mounted at. 695 | 696 | Outputs the absolute path. 697 | 698 | ## Development 699 | 700 | Hacking on Dock is easy thanks to the fact that it is run within a Dock 701 | container! Provided you have Docker and Bash installed on your system, working 702 | on Dock is as easy as running `bin/dock` from the root of the repository. 703 | 704 | ### Running Tests 705 | 706 | Tests can be run by executing: 707 | 708 | ```bash 709 | bin/test 710 | ``` 711 | 712 | ...from the root of the repository. This will start a Dock container and run 713 | all tests, which are written in Bash using [Bats](https://github.com/sstephenson/bats). 714 | 715 | To run a specific test or set of tests, execute: 716 | 717 | ```bash 718 | bin/test test/path/to/test.bats test/path/to/another.bats 719 | ``` 720 | 721 | ## Change Log 722 | 723 | If you're interested in seeing a summarized list of changes between each version 724 | of Dock, see the [Dock Change Log](CHANGELOG.md) 725 | 726 | ## License 727 | 728 | Dock is released under the [Apache 2.0 license](LICENSE). 729 | -------------------------------------------------------------------------------- /bin/dock: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # START USAGE DOCUMENTATION 4 | # dock is a tool for defining, building, and running self-contained 5 | # development environments inside Docker containers. 6 | # 7 | # Usage: dock [options] [command] 8 | # -a Attach to already-running container 9 | # 10 | # -c config-file Configuration file to use (default is .dock) 11 | # 12 | # -d Detach and run container in the background 13 | # 14 | # -e Extend an existing Dock container with a new project 15 | # 16 | # -t Terraform an extended Dock container 17 | # 18 | # -f Force creation of new container (destroying any 19 | # already-existing container). 20 | # -q Don't display any Dock-specific output (just output from 21 | # the command you are running in the container) 22 | # -v Display version information. 23 | # -V Display extended version information (for bug reports) 24 | # END USAGE DOCUMENTATION 25 | 26 | set -euo pipefail 27 | 28 | dock_version="1.4.8" 29 | dock_bin="$0" 30 | 31 | # We want all output to go to STDERR so that STDOUT receives output from the 32 | # original command. This makes shebang Dock scripts more useful as they will 33 | # appear to act exactly like the original script (if you ignore STDERR). 34 | redirect_stdout() { 35 | exec 5<&1 36 | exec 1>&2 37 | } 38 | redirect_stdout 39 | 40 | restore_stdout() { 41 | exec 1>&5 42 | } 43 | 44 | default_conf_file=".dock" 45 | default_compose_file="docker-compose.yml" 46 | 47 | # We use \033 instead of \e since Macs don't support \e as of Mountain Lion 48 | red="\033[0;31m" 49 | green="\033[0;32m" 50 | yellow="\033[0;33m" 51 | cyan="\033[0;36m" 52 | reset="\033[0m" 53 | 54 | error() { 55 | quiet && return || true 56 | if interactive; then 57 | echo -en "${red}ERROR${reset}: " 58 | else 59 | echo -n "ERROR: " 60 | fi 61 | echo "$@" 62 | } 63 | 64 | warn() { 65 | quiet && return || true 66 | if interactive; then 67 | echo -en "${yellow}WARN${reset}: " 68 | else 69 | echo -n "WARN: " 70 | fi 71 | echo "$@" 72 | } 73 | 74 | notice() { 75 | quiet && return || true 76 | if interactive; then 77 | echo -e "${cyan}$@${reset}" 78 | else 79 | echo "$@" 80 | fi 81 | } 82 | 83 | info() { 84 | quiet && return || true 85 | echo "$@" 86 | } 87 | 88 | success() { 89 | quiet && return || true 90 | if interactive; then 91 | echo -e "${green}$@${reset}" 92 | else 93 | echo "$@" 94 | fi 95 | } 96 | 97 | in_path() { 98 | [ "$(type -t "$1")" = file ] && command -v "$1" >/dev/null 2>&1 99 | } 100 | 101 | display_debug_version_info() { 102 | echo "Dock: $(dock_version)" 103 | echo -n "Docker: " 104 | if in_path docker; then 105 | echo "$(docker --version)" 106 | else 107 | echo "Docker executable not found in PATH!" 108 | fi 109 | echo "Bash: $BASH_VERSION" 110 | echo "OS: $(uname -a)" 111 | } 112 | 113 | display_usage() { 114 | start_line="$(grep -n 'START USAGE DOCUMENTATION' $dock_bin | head -n1 | cut -d: -f1)" 115 | end_line="$(grep -n 'END USAGE DOCUMENTATION' $dock_bin | head -n1 | cut -d: -f1)" 116 | tail -n+$(expr $start_line + 1) $dock_bin | head -n$(expr $end_line - $start_line - 1) | sed 's|# ||' | sed 's|#||' || true 117 | } 118 | 119 | ask() { 120 | local question="$1" 121 | local default="$2" 122 | local var_name="$3" 123 | if interactive && ! quiet; then 124 | echo -en "${cyan}${question}${reset} [$default] " 125 | read $var_name 126 | [ -z "${!var_name}" ] && eval $var_name="$default" || true 127 | else 128 | eval $var_name="$default" 129 | fi 130 | } 131 | 132 | display_extension_completion_msg() { 133 | cat < 160 | 161 | You can restart services by executing: 162 | 163 | docker exec $container_name docker-compose restart [$services] 164 | 165 | ============================================================== 166 | MSG 167 | } 168 | 169 | group_id() { 170 | id -g 171 | } 172 | 173 | user_id() { 174 | id -u 175 | } 176 | 177 | attach_command() { 178 | if [ "${#@}" -eq 0 ]; then 179 | error "Must specify at least one argument for attach command!" 180 | return 1 181 | else 182 | attach_command_args=("${@}") 183 | fi 184 | } 185 | 186 | build_arg() { 187 | if [ -z "${1+x}" ]; then 188 | error "Must provide name and value for build argument!" 189 | return 1 190 | elif [ -z "${2+x}" ]; then 191 | error "Must provide value for build argument $1!" 192 | return 1 193 | else 194 | build_flags "--build-arg" "$1=$2" 195 | fi 196 | } 197 | 198 | build_context() { 199 | if [ -z "${1+x}" ]; then 200 | echo "${build_context}" 201 | else 202 | build_context="${repo_root}/$1" 203 | fi 204 | } 205 | 206 | build_flags() { 207 | if [ "${#@}" -gt 0 ]; then 208 | build_args+=("$@") 209 | else 210 | error "Must provide one or more arguments for build_flags!" 211 | return 1 212 | fi 213 | } 214 | 215 | container_name() { 216 | if [ -z "${1+x}" ]; then 217 | # No argument specified, so return the current name 218 | echo "${container_name}" 219 | else 220 | # Otherwise set the current name 221 | if [ -z "$1" ]; then 222 | error "Cannot specify an empty name for container!" 223 | return 1 224 | fi 225 | container_name="$1" 226 | fi 227 | } 228 | 229 | default_command() { 230 | if [ "${#@}" -eq 0 ]; then 231 | error "Must specify at least one argument for default command!" 232 | return 1 233 | else 234 | command_args=("${@}") 235 | fi 236 | } 237 | 238 | detach_keys() { 239 | if [ -z "${1+x}" ]; then 240 | error "Must provide key sequence as argument!" 241 | return 1 242 | else 243 | detach_keys="$1" 244 | fi 245 | } 246 | 247 | dockerfile() { 248 | if [ -z "${1+x}" ]; then 249 | error "Must provide path to Dockerfile as argument!" 250 | return 1 251 | else 252 | dockerfile="$1" 253 | fi 254 | } 255 | 256 | dock_in_dock() { 257 | if [ -z "${1+x}" ]; then 258 | error "Must provide true/false as argument to dock_in_dock!" 259 | return 1 260 | else 261 | dock_in_dock="$1" 262 | fi 263 | } 264 | 265 | dock_version() { 266 | echo "$dock_version" 267 | } 268 | 269 | entrypoint() { 270 | if [ -z "${1+x}" ]; then 271 | error "Must provide path to entrypoint executable as argument!" 272 | return 1 273 | else 274 | entrypoint="$1" 275 | fi 276 | } 277 | 278 | container_hostname() { 279 | if [ -z "${1+x}" ]; then 280 | # No argument specified, so return the current name if defined 281 | if [ -n "${container_hostname+x}" ]; then 282 | echo "${container_hostname}" 283 | else 284 | error "You must set an explicit hostname first!" 285 | return 1 286 | fi 287 | else 288 | # Otherwise set the current name 289 | if [ -z "$1" ]; then 290 | error "Cannot specify an empty name for hostname!" 291 | return 1 292 | fi 293 | container_hostname="$1" 294 | fi 295 | } 296 | 297 | image() { 298 | if [ -z "${1+x}" ]; then 299 | error "Must provide image name as argument!" 300 | return 1 301 | else 302 | image="$1" 303 | fi 304 | } 305 | 306 | startup_services() { 307 | if [ -z "${1+x}" ]; then 308 | error "Must provide list of services (e.g service mysql)!" 309 | return 1 310 | else 311 | startup_services="$1" 312 | fi 313 | } 314 | 315 | pull_latest() { 316 | if [ -z "${1+x}" ] || "$1"; then 317 | pull=true 318 | else 319 | pull=false 320 | fi 321 | } 322 | 323 | osx() { 324 | [ "$(uname)" = Darwin ] 325 | } 326 | 327 | linux() { 328 | [ "$(uname)" = Linux ] 329 | } 330 | 331 | interactive() { 332 | [ -t 0 ] 333 | } 334 | 335 | repo_path() { 336 | echo "${repo_root}" 337 | } 338 | 339 | detach() { 340 | # If called without any arguments, assume "true" 341 | if [ -z "${1+x}" ] || $1; then 342 | detach=true 343 | else 344 | detach=false 345 | fi 346 | } 347 | 348 | env_var() { 349 | if [ -z "${1+x}" ]; then 350 | error "Must provide name and value for environment variable!" 351 | return 1 352 | elif [ -z "${2+x}" ]; then 353 | error "Must provide value for environment variable $1!" 354 | return 1 355 | else 356 | env+=("$1=$2") 357 | fi 358 | } 359 | 360 | optional_env_var() { 361 | if [ -z "${1+x}" ]; then 362 | error "Must provide name of optional environment variable!" 363 | return 1 364 | else 365 | optional_env+=("$1") 366 | fi 367 | } 368 | 369 | privileged() { 370 | if [ -z "${1+x}" ]; then 371 | # If called without any arguments, return whether it is privileged 372 | $privileged 373 | elif "$1"; then 374 | privileged=true 375 | else 376 | privileged=false 377 | fi 378 | } 379 | 380 | publish() { 381 | if [ -z "${1+x}" ]; then 382 | error "Must provide port publish specification as argument!" 383 | return 1 384 | else 385 | exposed_ports+=("$1") 386 | fi 387 | } 388 | 389 | quiet() { 390 | [ -n "${quiet+x}" ] && $quiet 391 | } 392 | 393 | required_env_var() { 394 | if [ -z "${1+x}" ]; then 395 | error "Must provide name of required environment variable!" 396 | return 1 397 | else 398 | required_env+=("$1") 399 | fi 400 | } 401 | 402 | run_flags() { 403 | if [ "${#@}" -gt 0 ]; then 404 | run_args+=("$@") 405 | else 406 | error "Must provide one or more arguments for run_flags!" 407 | return 1 408 | fi 409 | } 410 | 411 | volume() { 412 | if [ -z "${1+x}" ]; then 413 | error "Must provide volume specification!" 414 | return 1 415 | else 416 | volumes+=("$1") 417 | fi 418 | } 419 | 420 | label() { 421 | if [ "${#@}" -ne 2 ]; then 422 | error "Must provide exactly two arguments for label key, value pair" 423 | return 1 424 | else 425 | labels+=( "$1=$2" ) 426 | fi 427 | } 428 | 429 | workspace_path() { 430 | if [ -z "${1+x}" ]; then 431 | # No argument specified, so return the current name 432 | echo "${workspace_dir}" 433 | else 434 | # Otherwise set the current name 435 | if [ -z "$1" ]; then 436 | error "Cannot specify an empty path for workspace_path!" 437 | return 1 438 | fi 439 | workspace_dir="$1" 440 | fi 441 | } 442 | 443 | container_running() { 444 | target_container=$container_name 445 | if [ -n "${1+x}" ]; then 446 | target_container="$1" 447 | fi 448 | 449 | [ "$(docker inspect --format '{{ .State.Status }}' $target_container 2>&1)" = "running" ] 450 | } 451 | 452 | container_exists() { 453 | target_container=$container_name 454 | if [ -n "${1+x}" ]; then 455 | target_container="$1" 456 | fi 457 | 458 | docker inspect $target_container >/dev/null 2>&1 459 | } 460 | 461 | image_is_local() { 462 | if [ -z "${1+x}" ]; then 463 | error "Must provide image specification" 464 | return 1 465 | fi 466 | 467 | if ! docker images --format '{{.Repository}}:{{.Tag}}' | grep $1 > /dev/null 2>&1; then 468 | notice "Image $1 not found within local docker image repository." 469 | return 1 470 | fi 471 | 472 | info "Image $1 found within local docker image repository" 473 | } 474 | 475 | pull_image() { 476 | if [ -z "${1+x}" ]; then 477 | error "Must provide image specification" 478 | return 1 479 | fi 480 | 481 | local image=$1 482 | notice "Ensuring we have the latest version of $image locally..." 483 | pull_args=("docker" "pull" "$image") 484 | if quiet; then 485 | "${pull_args[@]}" 2>&1 >/dev/null 486 | else 487 | "${pull_args[@]}" 488 | fi 489 | 490 | return $? 491 | } 492 | 493 | source_script() { 494 | local file=$1 495 | if [ -e "$file" ]; then 496 | if ! source "$file"; then 497 | error "Problem sourcing $file" 498 | return 1 499 | fi 500 | else 501 | error "File '$file' does not exist!" 502 | return 1 503 | fi 504 | } 505 | 506 | is_valid_docker_compose() { 507 | local file=$1 508 | if [ -e "$file" ]; then 509 | if ! docker-compose config 2>&1>/dev/null; then 510 | error "Invalid docker-compose schema detected!" 511 | return 1 512 | fi 513 | else 514 | error "$file does NOT exist!" 515 | return 1 516 | fi 517 | } 518 | 519 | extend_container() { 520 | if [ -z "${1+x}" ]; then 521 | error "Must provide a Dock container ID to extend!" 522 | return 1 523 | fi 524 | 525 | notice "Extending Dock: $1..." 526 | # Set image and container names accordingly 527 | container_name "$(convert_to_valid_container_name $1)" 528 | extended_image="$container_name:dock" 529 | if [ -z "$image" ]; then 530 | image "$extended_image" 531 | fi 532 | 533 | # Set workspace directory to project repo root 534 | workspace_path "$(pwd)" 535 | 536 | # Automatically detach from container to allow for build process to continue and 537 | # saving of intermediate container state as image to occur 538 | detach true 539 | 540 | # Silence default_commands specified by projects during extension due to unexpected 541 | # behavior stemming from operating on an intermediary environment state 542 | # TODO: Repurpose default_command with something more constructive (e.g. setting up 543 | # a virtual dev environment for the project involved in the extension). 544 | default_command tail -f /dev/null 545 | 546 | # add Dock environment construction labels 547 | label "dock.${project}" "$(pwd)/.dock" 548 | # Projects do not necessarily need to supply a docker-compose file so only add a 549 | # label if the file exists 550 | if [ -e "$(pwd)/docker-compose.yml" ]; then 551 | label "compose.${project}" "$(pwd)/${default_compose_file}" 552 | else 553 | warn "Unable to locate a $default_compose_file file for ${project}." 554 | fi 555 | 556 | # Update the container's project list 557 | existing_projects="$(get_label_value $container_name projects 2>/dev/null)" 558 | p_list="${existing_projects} $project" 559 | p_list="$(echo $p_list | xargs -n1 | sort -u | xargs)" 560 | label "projects" "$p_list" 561 | 562 | # Record the list of services to startup for this project and 563 | # append to startup services list for container environment 564 | existing_services="$(get_label_value $container_name startup_services 2>/dev/null)" 565 | s="${existing_services:-''} ${startup_services:-''}" 566 | s="$(echo $s | xargs -n1 | sort -u | xargs)" 567 | label "startup_services" "$s" 568 | 569 | # proceed to launch extended dock container... 570 | # TODO: refactor below common setup (i.e. build of docker run args) to remove code 571 | # duplication 572 | # 573 | # If we're already inside a Dock environment, just execute the command. 574 | # This allows us to reuse scripts with dock in their shebang line in other 575 | # Dock-ified scripts without invoking dock-within-dock (which is likely not what 576 | # we want). 577 | if [ -n "${INSIDE_DOCK+x}" ]; then 578 | exec "${command_args[@]}" 579 | fi 580 | 581 | # Adding workspace directory as an enviroment variable. Since extending actually mounts the 582 | # host file system path it needs to be set explicitly and not be set as the default /workdir 583 | workspace_dir=$PWD 584 | 585 | # Compile run args based on current configuration 586 | compile_run_args 587 | 588 | # If the targeted dock environment does not exist, create a new image 589 | # based on the dock configuration of the current project 590 | temp_container_name="temp" 591 | if ! container_exists $container_name &>/dev/null; then 592 | if [ -n "$dockerfile" ]; then 593 | info "Dock container $container_name not found, creating $container_name with ${project}..." 594 | if $pull; then 595 | build_args+=("--pull") 596 | fi 597 | build_args+=("--file" "$dockerfile" "--tag" "$image" "$build_context") 598 | if quiet; then 599 | "${build_args[@]}" 2>&1 >/dev/null 600 | else 601 | "${build_args[@]}" 602 | fi 603 | notice "$dockerfile built into $image!" 604 | elif [ -z "$image" ]; then 605 | error "Must specify either an image to run or a Dockerfile to build and run!" 606 | info "(is there a $default_conf_file file in your current directory?)" 607 | return 1 608 | fi 609 | else 610 | # Target container matches existing Dock container, reuse... 611 | # Rename existing container to $temp_container_name so that we can launch the replacement container with 612 | # the exact same name and still have access to the previous version of the container's 613 | # volumes 614 | info "Dock container $container_name found, extending with ${project}..." 615 | image "$extended_image" 616 | docker rename $container_name $temp_container_name 617 | run_args+=("--volumes-from" "$temp_container_name") 618 | fi 619 | 620 | run_args+=("$image") 621 | if [ "${#command_args[@]}" -gt 0 ]; then 622 | run_args+=("${command_args[@]}") 623 | fi 624 | 625 | restore_stdout 626 | "${run_args[@]}" 627 | if [ $? -ne 0 ]; then 628 | return 1 629 | fi 630 | 631 | docker commit $container_name "$extended_image" >/dev/null 632 | # Cleanup intermediary temporary container if necessary 633 | if container_exists $temp_container_name &>/dev/null; then 634 | docker stop $temp_container_name &>/dev/null || true 635 | docker rm $temp_container_name &>/dev/null || true 636 | fi 637 | 638 | success "$container_name has successfully been extended with $project!" 639 | notice "project list: $(get_label_value $container_name projects)" 640 | display_extension_completion_msg 641 | } 642 | 643 | get_labels() { 644 | if [ -z "${1+x}" ]; then 645 | error "Must provide docker object to inspect!" 646 | return 1 647 | fi 648 | 649 | labels=$(docker inspect --format='{{json .Config.Labels}}' "$1") 650 | echo "$labels" 651 | } 652 | 653 | get_label_keys() { 654 | if [ -z "${1+x}" ]; then 655 | error "Must provide docker object to inspect!" 656 | return 1 657 | fi 658 | 659 | local keyword="" 660 | if [ -n "${2+x}" ]; then 661 | keyword="$2" 662 | fi 663 | 664 | keys=$(docker inspect --format='{{json .Config.Labels}}' "$1" | jq --arg k $keyword \ 665 | '. | keys[] | select(startswith($k))' | sed -e 's/^"//' -e 's/"$//') 666 | echo "$keys" 667 | } 668 | 669 | get_label_value() { 670 | if [ -z "${1+x}" ]; then 671 | error "Must provide docker object to inspect!" 672 | return 1 673 | fi 674 | 675 | if [ -z "${2+x}" ]; then 676 | error "Must provide label key to target!" 677 | return 1 678 | fi 679 | 680 | obj=$1 681 | key=$2 682 | value=$(docker inspect --format='{{json .Config.Labels}}' $obj | jq --arg k $key \ 683 | '.[$k]' | sed -e 's/^"//' -e 's/"$//') 684 | 685 | # Remove string escapes applied by docker's label logic and return 686 | echo -e "$(echo -e "$value" | sed -e 's/\\\"/"/g')" 687 | } 688 | 689 | terraform_container() { 690 | if [ -z "${1+x}" ]; then 691 | error "Must provide a Dock container ID to extend!" 692 | return 1 693 | fi 694 | local container_name="$(convert_to_valid_container_name $1)" 695 | 696 | notice "Terraforming Dock: $container_name..." 697 | projects_to_compose=$(get_label_keys $container_name "compose.") 698 | 699 | # Recreate temporary workspace for new terraform operation 700 | tmp_workspace="/tmp/compose" 701 | docker exec $container_name rm --recursive --force $tmp_workspace 702 | docker exec $container_name mkdir $tmp_workspace 703 | 704 | compose_args=("COMPOSE_HTTP_TIMEOUT=600 docker-compose") 705 | # Resolve project docker-compose files and cache in temporary workspace 706 | for project in ${projects_to_compose[@]}; do 707 | compose_path=$(get_label_value $container_name $project) 708 | local compose_dir=$(dirname $compose_path) 709 | local output_file="${project}.yml" 710 | docker exec $container_name bash -c "\ 711 | # change directory to project compose file location 712 | cd $compose_dir; 713 | # resolve project compose file 714 | docker-compose config > ${tmp_workspace}/$output_file" 715 | compose_args+=("--file" "$output_file") 716 | done 717 | # Only start services which have been defined as startup services by composed 718 | # projects 719 | local services="$(get_label_value $container_name startup_services)" 720 | compose_args+=("up" "--build" "-d" "$services") 721 | 722 | info "Terraforming and recomposing Dock environment..." 723 | # Purge all existing containers within Dock environment 724 | docker exec $container_name bash -c "\ 725 | docker stop $(docker ps -aq) >/dev/null 2>&1 || true; 726 | docker rm $(docker ps -aq) >/dev/null 2>&1 || true" 727 | 728 | # Compose environment from temporary workspace 729 | compose_cmd="${compose_args[@]}" 730 | docker exec $container_name bash -c "\ 731 | # change directory to temporary workspace 732 | cd $tmp_workspace; 733 | # execute environment composition/merge 734 | $compose_cmd" 735 | 736 | success "Dock environment successfully terraformed!" 737 | display_terraform_completion_msg 738 | } 739 | 740 | convert_to_valid_container_name() { 741 | # Container names are only allowed to contain characters from [a-zA-Z0-9_-] 742 | # so ensure that the input does not contain chars like /,: 743 | # This method isn't exhaustive and currently just handles its current use case 744 | if [ -z "${1+x}" ]; then 745 | error "Must provide microenvironment ID to extend" 746 | return 1 747 | fi 748 | 749 | name=$1 750 | # remove ':'s 751 | name="${name//:/_}" 752 | # remove '/'s 753 | name="${name////-}" 754 | 755 | if [ "$name" != "$1" ]; then 756 | notice "The provided container name, ${1}, is not in a valid format - converted to ${name}." 757 | fi 758 | echo $name 759 | } 760 | 761 | destroy_container() { 762 | target_container=$container_name 763 | if [ -n "${1+x}" ]; then 764 | target_container="$1" 765 | fi 766 | 767 | if container_running $target_container; then 768 | docker stop $target_container >/dev/null || true 769 | fi 770 | if container_running; then 771 | docker kill $target_container >/dev/null || true 772 | fi 773 | if container_exists; then 774 | docker rm --force $target_container >/dev/null 775 | fi 776 | } 777 | 778 | port_taken_on_localhost() { 779 | if osx; then 780 | # show -a(ll sockets) and -n(umeric addresses) 781 | echo | lsof -n -i :$1 2>/dev/null | grep -i LISTEN >/dev/null 2>&1 782 | else 783 | echo | netstat --numeric --listening 2>/dev/null | grep $1 >/dev/null 2>&1 784 | fi 785 | } 786 | 787 | process_exposed_ports() { 788 | forwarded_ports=() # OS X: Ports forwarded from the local machine to the VM 789 | published_ports=() # Ports published by the container on the Docker host 790 | 791 | # Need to explicitly check length of array before accessing it 792 | if [ ${#exposed_ports[@]} -eq 0 ]; then 793 | return 794 | fi 795 | 796 | for port_spec in ${exposed_ports[@]}; do 797 | colons="$(grep -o ':' <<< "$port_spec" | wc -l | tr -d '[[:space:]]')" 798 | case $colons in 799 | 0) 800 | warn "Ignoring port specification $port_spec as it does not specify the host port" 801 | ;; 802 | 1) 803 | local host_port="$(cut -d: -f1 <<< "$port_spec")" 804 | 805 | if port_taken_on_localhost $host_port; then 806 | warn "Ignoring port specification $port_spec since another process has already bound to localhost:$host_port" 807 | warn "You're likely already running the service locally." 808 | else 809 | published_ports+=("$port_spec") 810 | fi 811 | ;; 812 | 2) 813 | warn "Ignoring port specification since it contains a specific host address: '$port_spec'" 814 | ;; 815 | *) 816 | error "Invalid port specification: '$port_spec'" 817 | return 1 818 | ;; 819 | esac 820 | done 821 | } 822 | 823 | resolve_symlink() { 824 | local file="$1" 825 | 826 | ( 827 | cd $(dirname "$file") 828 | file="$(basename "$file")" 829 | 830 | # Iterate down (possible) chain of symlinks 831 | while [ -L "$file" ]; do 832 | file="$(readlink "$file")" 833 | cd "$(dirname "$file")" 834 | file="$(basename "$file")" 835 | done 836 | 837 | physical_dir="$(pwd -P)" 838 | echo $physical_dir/$file 839 | ) 840 | } 841 | 842 | # We don't want to deal with managing all the user's dependencies, so check for 843 | # key ones and get them to install it however they prefer. 844 | check_dependencies() { 845 | if ! in_path docker; then 846 | error 'Cannot find `docker` executable in your path!' 847 | error 'Have you installed Docker on this machine?' 848 | return 1 849 | elif ! docker info >/dev/null 2>&1; then 850 | error 'Cannot connect to the Docker daemon' 851 | info 'Is the daemon running on this host?' 852 | info 'Does your user have permission to communicate with the Docker socket?' 853 | return 1 854 | fi 855 | } 856 | 857 | initialize_variables() { 858 | if ! git rev-parse --git-dir >/dev/null 2>&1; then 859 | error "You must be in a Git repository to run: $(basename $0) $@" 860 | return 1 861 | fi 862 | 863 | repo_root=$(pwd) 864 | repo=$(basename "$repo_root") 865 | repo=${repo//[^a-zA-Z0-9.-]/-} # Ensure slug is a valid name for Docker 866 | build_args=("docker" "build") 867 | build_context="${repo_root}" 868 | run_args=("docker" "run") 869 | dockerfile="" 870 | image="" 871 | detach=false 872 | detach_keys="ctrl-x,x" # Ctrl-P is a useful shortcut when using Bash 873 | dock_in_dock=false # Don't create recursive Dock containers by default 874 | pull=true 875 | privileged=true 876 | env=() 877 | optional_env=() 878 | required_env=() 879 | container_name="$repo-dock" 880 | volumes=("$(resolve_symlink $dock_bin):/usr/local/bin/dock") 881 | labels=() 882 | exposed_ports=() 883 | workspace_dir="/workspace" 884 | force_tty=false 885 | attach_command_args=("sh") 886 | command_args=() 887 | 888 | # When running on a Mac, all Docker commands are actually run as a user on a VM 889 | # which has a different UID/GID than your Mac user. Set helper variables which 890 | # can be used in .dock files to use the correct UID/GID without having to know 891 | # whether developer is running Mac or Linux 892 | current_uid="$(id -u)" 893 | current_gid="$(id -g)" 894 | 895 | # Since the configuration file must be sourced before command line arguments are 896 | # processed (in order to allow command line args to override config), we have to 897 | # do a separate argument parse step to determine the custom config file. 898 | dock_file="$default_conf_file" 899 | explicit_dock_config=false 900 | while getopts ":c:" opt; do 901 | case $opt in 902 | c) 903 | dock_file="$OPTARG" 904 | explicit_dock_config=true 905 | ;; 906 | esac 907 | done 908 | OPTIND=1 # Reset index so that we can parse the arguments with getopts again 909 | 910 | # Load additional variables from config file 911 | # (useful for defining default image/dockerfile). 912 | # Command line arguments will override these if present. 913 | if [ -e "$dock_file" ]; then 914 | if ! source_script "$dock_file"; then 915 | error "Problem sourcing $dock_file" 916 | return 1 917 | fi 918 | elif $explicit_dock_config; then 919 | error "Dock configuration file '$dock_file' does not exist!" 920 | return 1 921 | fi 922 | 923 | # set project to repo name if unset in dock config 924 | project="${project:=$(basename $(pwd))}" 925 | } 926 | 927 | attach_to_container() { 928 | target_container=$container_name 929 | if [ -n "${1+x}" ]; then 930 | target_container="$1" 931 | fi 932 | 933 | if container_running $target_container; then 934 | exec_args=("docker" "exec" "--interactive" "--tty") 935 | if $privileged; then 936 | exec_args+=("--privileged") 937 | fi 938 | exec_args+=("$target_container" "${attach_command_args[@]}") 939 | 940 | exec "${exec_args[@]}" 941 | elif container_exists; then 942 | error "Container $container_name exists but is not running, so you can't attach." 943 | return 1 944 | else 945 | error "No container named $container_name is currently running." 946 | error "You must start the container first before you can attach!" 947 | return 1 948 | fi 949 | } 950 | 951 | check_for_existing_container() { 952 | target_container=$container_name 953 | if [ -n "${1+x}" ]; then 954 | target_container="$1" 955 | fi 956 | 957 | if container_exists $target_container; then 958 | if [ "${DOCK_FORCE_DESTROY:-0}" = 1 ]; then 959 | notice "Destroying container $target_container..." 960 | destroy_container $target_container 961 | notice "Container $target_container destroyed." 962 | return 963 | fi 964 | 965 | if container_running $target_container; then 966 | error "Container $target_container is already running." 967 | if interactive; then 968 | ask "Attach to the container? (y/n)" n answer 969 | if [ "${answer}" = "y" ]; then 970 | attach_to_container "$target_container" 971 | else 972 | info "You answered '${answer}' instead of 'y'; not attaching." 973 | fi 974 | else 975 | info "You can attach to it by running \`dock -a\`." 976 | fi 977 | return 1 978 | else 979 | error "Container $target_container already exists but is stopped." 980 | if interactive; then 981 | ask "Destroy existing container and create new one? (y/n)" n answer 982 | if [ "${answer}" = "y" ]; then 983 | notice "Destroying container $target_container..." 984 | destroy_container $target_container 985 | notice "Container $target_container destroyed." 986 | return 987 | else 988 | info "You answered '${answer}' instead of 'y'; not attaching." 989 | fi 990 | else 991 | info "You can ensure it is destroyed before starting a new container by including the -f flag." 992 | fi 993 | return 1 994 | fi 995 | fi 996 | } 997 | 998 | compile_run_args() { 999 | if [ ${#optional_env[@]} -gt 0 ]; then 1000 | for var_name in "${optional_env[@]}"; do 1001 | if [ -n "${!var_name+x}" ]; then 1002 | env+=("${var_name}=${!var_name}") 1003 | fi 1004 | done 1005 | fi 1006 | 1007 | if [ ${#required_env[@]} -gt 0 ]; then 1008 | for var_name in "${required_env[@]}"; do 1009 | if [ -z "${!var_name+x}" ]; then 1010 | error "Environment variable ${var_name} is required but not set!" 1011 | exit 1 1012 | else 1013 | env+=("${var_name}=${!var_name}") 1014 | fi 1015 | done 1016 | fi 1017 | 1018 | run_args+=("--name" "$container_name") 1019 | run_args+=("--workdir" "$workspace_dir") 1020 | run_args+=("--detach-keys" "$detach_keys") 1021 | 1022 | if [ -n "${container_hostname+x}" ]; then 1023 | run_args+=("--hostname" "$container_hostname") 1024 | fi 1025 | 1026 | if [ -n "${entrypoint+x}" ]; then 1027 | run_args+=("--entrypoint" "$entrypoint") 1028 | fi 1029 | 1030 | if [ ${#env[@]} -gt 0 ]; then 1031 | for e in "${env[@]}"; do 1032 | run_args+=("--env" "$e") 1033 | done 1034 | fi 1035 | 1036 | if ! $dock_in_dock; then 1037 | run_args+=("--env" "INSIDE_DOCK=1") 1038 | fi 1039 | 1040 | run_args+=("--env" "WORKSPACE_DIR=${workspace_dir}") 1041 | 1042 | process_exposed_ports 1043 | if [ ${#published_ports[@]} -gt 0 ]; then 1044 | for p in "${published_ports[@]}"; do 1045 | run_args+=("--publish" "$p") 1046 | done 1047 | fi 1048 | 1049 | # Mount repository in the container 1050 | volumes+=("$repo_root:$workspace_dir:rw") 1051 | if [ ${#volumes[@]} -gt 0 ]; then 1052 | for v in "${volumes[@]}"; do 1053 | run_args+=("--volume" "$v") 1054 | done 1055 | fi 1056 | 1057 | if [ ${#labels[@]} -gt 0 ]; then 1058 | for l in "${labels[@]}"; do 1059 | run_args+=("--label" "$l") 1060 | done 1061 | fi 1062 | 1063 | if $detach; then 1064 | run_args+=("--detach") 1065 | else 1066 | # Otherwise keep STDIN open and auto-remove the container on exit 1067 | run_args+=("--interactive" "--rm") 1068 | fi 1069 | 1070 | # Default to enabling --tty flag if STDIN (fd 0) is a TTY 1071 | # `docker run` will fail if we specify this without a TTY being present 1072 | if $force_tty || interactive; then 1073 | run_args+=("--tty") 1074 | fi 1075 | 1076 | if $privileged; then 1077 | run_args+=("--privileged") 1078 | fi 1079 | } 1080 | 1081 | ################################################################################ 1082 | 1083 | if [ -n "${DEBUG+x}" ]; then 1084 | set -x 1085 | fi 1086 | 1087 | if [ -n "${QUIET+x}" ] && [ "$QUIET" -eq 1 ]; then 1088 | quiet=true 1089 | fi 1090 | 1091 | # Need to scan for help flag before running `check_dependencies` since user 1092 | # may be trying to just view documentation and doesn't care if dependencies are 1093 | # installed. 1094 | while getopts ":hqvV" opt; do 1095 | case $opt in 1096 | h) 1097 | display_usage 1098 | exit 1099 | ;; 1100 | q) 1101 | quiet=true 1102 | ;; 1103 | v) 1104 | echo "$(dock_version)" 1105 | exit 1106 | ;; 1107 | V) 1108 | display_debug_version_info 1109 | exit 1110 | ;; 1111 | esac 1112 | done 1113 | OPTIND=1 # Reset index so that we can parse the arguments with getopts again 1114 | 1115 | check_dependencies 1116 | 1117 | # Need to pass original arguments so argument processing works 1118 | initialize_variables "$@" 1119 | while getopts "ac:de:fqt:" opt; do 1120 | case $opt in 1121 | a) 1122 | attach_to_container 1123 | ;; 1124 | c) 1125 | # Already processed earlier. Here to avoid parser warnings. 1126 | ;; 1127 | t) 1128 | terraform_container "$OPTARG" 1129 | exit 1130 | ;; 1131 | d) 1132 | detach=true 1133 | ;; 1134 | e) 1135 | extend_container "$OPTARG" 1136 | exit 1137 | ;; 1138 | f) 1139 | destroy_container 1140 | ;; 1141 | q) 1142 | # Already processed earlier. Here to avoid parser warnings. 1143 | ;; 1144 | /?) 1145 | error "Invalid option -$opt" 1146 | display_usage 1147 | exit 1 1148 | ;; 1149 | esac 1150 | done 1151 | 1152 | if [ $# -ge $OPTIND ]; then 1153 | # Set command to remaining unparsed arguments 1154 | # (overrides anything that was defined in $dock_file) 1155 | command_args=("${@:$OPTIND}") 1156 | attach_command "${@:$OPTIND}" 1157 | fi 1158 | 1159 | # If we're already inside a Dock environment, just execute the command. 1160 | # This allows us to reuse scripts with dock in their shebang line in other 1161 | # Dock-ified scripts without invoking dock-within-dock (which is likely not what 1162 | # we want). 1163 | if [ -n "${INSIDE_DOCK+x}" ]; then 1164 | exec "${command_args[@]}" 1165 | fi 1166 | 1167 | check_for_existing_container 1168 | 1169 | compile_run_args 1170 | 1171 | if [ -n "$dockerfile" ]; then 1172 | image="$repo:dock" 1173 | 1174 | notice "Building $dockerfile into image $image..." 1175 | if $pull; then 1176 | build_args+=("--pull") 1177 | fi 1178 | build_args+=("--file" "$dockerfile" "--tag" "$image" "$build_context") 1179 | if quiet; then 1180 | "${build_args[@]}" 2>&1 >/dev/null 1181 | else 1182 | "${build_args[@]}" 1183 | fi 1184 | notice "$dockerfile built into $image!" 1185 | 1186 | elif [ -n "$image" ] && $pull; then 1187 | notice "Ensuring we have the latest version of $image locally..." 1188 | pull_args=("docker" "pull" "$image") 1189 | if quiet; then 1190 | "${pull_args[@]}" 2>&1 >/dev/null 1191 | else 1192 | "${pull_args[@]}" 1193 | fi 1194 | elif [ -z "$image" ]; then 1195 | error "Must specify either an image to run or a Dockerfile to build and run!" 1196 | info "(is there a $default_conf_file file in your current directory?)" 1197 | exit 1 1198 | fi 1199 | 1200 | run_args+=("$image") 1201 | if [ "${#command_args[@]}" -gt 0 ]; then 1202 | run_args+=("${command_args[@]}") 1203 | fi 1204 | 1205 | notice "Starting container $container_name from image $image" 1206 | 1207 | restore_stdout 1208 | 1209 | exec "${run_args[@]}" 1210 | -------------------------------------------------------------------------------- /bin/test: -------------------------------------------------------------------------------- 1 | #!bin/dock bash 2 | 3 | # When run from the root project directory as `bin/test`, runs all tests within 4 | # the Dock container. 5 | 6 | if [ "${#@}" -eq 0 ]; then 7 | # No arguments given, so run all tests 8 | exec bats --tap $(find test -name "*.bats") 9 | else 10 | exec bats "$@" 11 | fi 12 | -------------------------------------------------------------------------------- /doc/img/docker-for-mac-file-sharing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/civiccc/dock/f969aace193338c21c590f85e2ef9943ce4eb45d/doc/img/docker-for-mac-file-sharing.png -------------------------------------------------------------------------------- /script/entrypoint.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Entrypoint script that starts a Docker daemon inside the Dock container 4 | # for us so that it is always available. 5 | 6 | set -euo pipefail 7 | 8 | start_docker() { 9 | # Don't do anything if daemon already running 10 | if docker info >/dev/null 2>&1; then 11 | return 12 | fi 13 | 14 | sudo dockerd>/dev/null 2>&1 & 15 | dockerd_pid=$! 16 | 17 | local max_tries=5 18 | for i in {1..5}; do 19 | if docker info >/dev/null 2>&1; then 20 | break 21 | fi 22 | echo "Waiting for Docker daemon to start..." >&2 23 | sleep 1 24 | done 25 | 26 | if ! docker info >/dev/null 2>&1; then 27 | echo "Docker daemon failed to start!" >&2 28 | return 1 29 | fi 30 | } 31 | 32 | start_docker 33 | exec "$@" 34 | -------------------------------------------------------------------------------- /script/install-dock: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Installs/upgrades Dock in /usr/local/bin/dock, requiring sudo password only 4 | # if necessary. 5 | # 6 | # You can run it anywhere by executing: 7 | # 8 | # curl https://raw.githubusercontent.com/brigade/dock/master/script/install-dock | bash 9 | 10 | set -euo pipefail 11 | 12 | location=/usr/local/bin/dock 13 | sudo="" 14 | 15 | auth-sudo() { 16 | if ! sudo -n >/dev/null 2>&1; then 17 | echo "Enter your sudo password to install Dock in $location" 18 | sudo -v 19 | sudo="sudo" 20 | fi 21 | } 22 | 23 | if [ ! -d $(dirname $location) ]; then 24 | mkdir_cmd="mkdir -p $location" 25 | if ! $mkdir_cmd >/dev/null 2>&1; then 26 | auth-sudo 27 | sudo $mkdir_cmd 28 | fi 29 | fi 30 | 31 | if ! touch $location; then 32 | auth-sudo 33 | sudo touch $location 34 | sudo chown $(id -u):$(id -g) $location 35 | fi 36 | 37 | if ! chmod +x $location; then 38 | auth-sudo 39 | sudo chmod +x $location 40 | fi 41 | 42 | $sudo curl -L https://raw.githubusercontent.com/brigade/dock/master/bin/dock --output $location 43 | -------------------------------------------------------------------------------- /test/configuration/build_arg.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load ../utils 4 | 5 | setup() { 6 | destroy_all_containers 7 | original_dir="$(pwd)" 8 | cd "$(create_repo my-project)" 9 | } 10 | 11 | teardown() { 12 | cd "${original_dir}" 13 | } 14 | 15 | @test "when build_arg specified it injects the build arg to the docker build command" { 16 | file Dockerfile <<-EOF 17 | FROM alpine:latest 18 | ARG MY_VAR 19 | ENV MY_VAR \${MY_VAR} 20 | RUN echo -n \$MY_VAR > /etc/result 21 | EOF 22 | 23 | file .dock <<-EOF 24 | dockerfile Dockerfile 25 | build_arg MY_VAR "some value" 26 | EOF 27 | 28 | run dock -q cat /etc/result 29 | [ "$status" -eq 0 ] 30 | [ "$output" = "some value" ] 31 | } 32 | 33 | @test "when build_arg specified without any arguments it returns error" { 34 | file .dock <<-EOF 35 | image alpine:latest 36 | build_arg 37 | EOF 38 | 39 | run dock echo 40 | [ "$status" -eq 1 ] 41 | [[ "$output" =~ "Must provide name and value for build argument!" ]] 42 | } 43 | 44 | @test "when build_arg specified name but not value it returns error" { 45 | file .dock <<-EOF 46 | image alpine:latest 47 | build_arg MY_VAR 48 | EOF 49 | 50 | run dock echo 51 | [ "$status" -eq 1 ] 52 | [[ "$output" =~ "Must provide value for build argument MY_VAR!" ]] 53 | } 54 | -------------------------------------------------------------------------------- /test/configuration/build_context.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load ../utils 4 | 5 | setup() { 6 | destroy_all_containers 7 | original_dir="$(pwd)" 8 | cd "$(create_repo my-project)" 9 | } 10 | 11 | teardown() { 12 | cd "${original_dir}" 13 | } 14 | 15 | @test "when build_context not specified it uses repo root for build context" { 16 | file Dockerfile <<-EOF 17 | FROM alpine:latest 18 | COPY my-file /etc/my-file 19 | EOF 20 | 21 | file my-file <<-EOF 22 | Hello 23 | EOF 24 | 25 | file .dock <<-EOF 26 | dockerfile Dockerfile 27 | EOF 28 | 29 | run dock cat /etc/my-file 30 | [ "$status" -eq 0 ] 31 | [[ "$output" =~ "Hello" ]] 32 | } 33 | 34 | @test "when build_context specified it uses that path relative to repo root for build context" { 35 | mkdir -p context 36 | 37 | file context/Dockerfile <<-EOF 38 | FROM alpine:latest 39 | COPY my-file /etc/my-file 40 | EOF 41 | 42 | 43 | file context/my-file <<-EOF 44 | Hello 45 | EOF 46 | 47 | file .dock <<-EOF 48 | build_context context 49 | dockerfile context/Dockerfile 50 | EOF 51 | 52 | run dock cat /etc/my-file 53 | [ "$status" -eq 0 ] 54 | [[ "$output" =~ "Hello" ]] 55 | } 56 | -------------------------------------------------------------------------------- /test/configuration/build_flags.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load ../utils 4 | 5 | setup() { 6 | destroy_all_containers 7 | original_dir="$(pwd)" 8 | cd "$(create_repo my-project)" 9 | } 10 | 11 | teardown() { 12 | cd "${original_dir}" 13 | } 14 | 15 | @test "when build_flags specified it adds those args to the docker build command" { 16 | file Dockerfile <<-EOF 17 | FROM alpine:latest 18 | ARG MY_VAR 19 | ENV MY_VAR \${MY_VAR} 20 | RUN echo -n \$MY_VAR > /etc/result 21 | EOF 22 | 23 | file .dock <<-EOF 24 | dockerfile Dockerfile 25 | build_flags --build-arg MY_VAR=some-value 26 | EOF 27 | 28 | run dock -q cat /etc/result 29 | [ "$status" -eq 0 ] 30 | [ "$output" = "some-value" ] 31 | } 32 | 33 | @test "when build_flags specified with no arguments it returns error" { 34 | file Dockerfile <<-EOF 35 | FROM alpine:latest 36 | EOF 37 | 38 | file .dock <<-EOF 39 | dockerfile Dockerfile 40 | build_flags 41 | EOF 42 | 43 | run dock echo 44 | [ "$status" -eq 1 ] 45 | [[ "$output" =~ "Must provide one or more arguments for build_flags!" ]] 46 | } 47 | -------------------------------------------------------------------------------- /test/configuration/container_hostname.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load ../utils 4 | 5 | project_name=my-project 6 | 7 | setup() { 8 | destroy_all_containers 9 | original_dir="$(pwd)" 10 | cd "$(create_repo ${project_name})" 11 | } 12 | 13 | teardown() { 14 | cd "${original_dir}" 15 | } 16 | 17 | @test "when container_hostname given no arguments and hostname not set it errors" { 18 | file .dock <<-EOF 19 | image alpine:latest 20 | if container_hostname; then 21 | touch hostname-successful 22 | else 23 | touch hostname-failed 24 | fi 25 | EOF 26 | 27 | run dock echo 28 | echo "$output" 29 | [ -e hostname-failed ] 30 | } 31 | 32 | @test "when container_hostname given no arguments and hostname set it returns hostname" { 33 | file .dock <<-EOF 34 | image alpine:latest 35 | container_hostname my-hostname 36 | echo "\$(container_hostname)" > hostname 37 | EOF 38 | 39 | run dock echo 40 | [ "$status" -eq 0 ] 41 | [ "$(cat hostname)" = my-hostname ] 42 | } 43 | 44 | @test "when hostname given an argument it sets the hostname of the container" { 45 | file .dock <<-EOF 46 | image alpine:latest 47 | container_hostname my-custom-name 48 | publish 5555:5555 49 | EOF 50 | 51 | run dock -q sh -c 'echo -n "$(hostname)"' 52 | [ "$status" -eq 0 ] 53 | [ "$output" = "my-custom-name" ] 54 | } 55 | -------------------------------------------------------------------------------- /test/configuration/container_name.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load ../utils 4 | 5 | project_name=my-project 6 | 7 | setup() { 8 | destroy_all_containers 9 | original_dir="$(pwd)" 10 | cd "$(create_repo ${project_name})" 11 | } 12 | 13 | teardown() { 14 | cd "${original_dir}" 15 | } 16 | 17 | @test "when container_name given no arguments it returns the name of the container" { 18 | file .dock <<-EOF 19 | image alpine:latest 20 | echo "\$(container_name)" > container_name 21 | EOF 22 | 23 | run dock echo 24 | [ "$status" -eq 0 ] 25 | [ "$(cat container_name)" = my-project-dock ] 26 | } 27 | 28 | @test "when container_name given an argument it sets the name of the container" { 29 | file .dock <<-EOF 30 | image alpine:latest 31 | container_name my-custom-name 32 | publish 5555:5555 33 | EOF 34 | 35 | run dock echo 36 | [ "$status" -eq 0 ] 37 | 38 | dock -d nc -l -s 0.0.0.0 -p 5555 # Waits until connection opened to netcat 39 | [ "$(docker ps --quiet --filter name=my-custom-name | wc -l)" -eq 1 ] 40 | echo | nc 127.0.0.1 5555 # Stop the container by opening connection 41 | } 42 | -------------------------------------------------------------------------------- /test/configuration/dockerfile.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load ../utils 4 | 5 | setup() { 6 | destroy_all_containers 7 | original_dir="$(pwd)" 8 | cd "$(create_repo my-project)" 9 | } 10 | 11 | teardown() { 12 | cd "${original_dir}" 13 | } 14 | 15 | @test "when dockerfile not specified it returns an error" { 16 | file .dock <<-EOF 17 | # An otherwise empty configuration 18 | EOF 19 | 20 | run dock echo 21 | [ "$status" -eq 1 ] 22 | [[ "$output" =~ "Must specify either an image to run or a Dockerfile to build and run!" ]] 23 | } 24 | 25 | @test "when dockerfile specified it builds and starts container with the image" { 26 | file Dockerfile <<-EOF 27 | FROM alpine:latest 28 | RUN echo Hello > /etc/hello-world 29 | EOF 30 | 31 | file .dock <<-EOF 32 | dockerfile Dockerfile 33 | EOF 34 | 35 | run dock echo /etc/hello-world 36 | [ "$status" -eq 0 ] 37 | [[ "$output" =~ "Hello" ]] 38 | } 39 | 40 | @test "when dockerfile specified along with build_context it set relative to repo root, not build context" { 41 | mkdir -p context 42 | 43 | file context/Dockerfile <<-EOF 44 | FROM alpine:latest 45 | RUN echo Hello > /etc/hello-world 46 | EOF 47 | 48 | file .dock <<-EOF 49 | build_context context 50 | dockerfile context/Dockerfile 51 | EOF 52 | 53 | run dock echo /etc/hello-world 54 | [ "$status" -eq 0 ] 55 | [[ "$output" =~ "Hello" ]] 56 | } 57 | -------------------------------------------------------------------------------- /test/configuration/env_var.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load ../utils 4 | 5 | setup() { 6 | destroy_all_containers 7 | original_dir="$(pwd)" 8 | cd "$(create_repo)" 9 | } 10 | 11 | teardown() { 12 | cd "${original_dir}" 13 | } 14 | 15 | @test "when env_var specified without any arguments it returns error" { 16 | file .dock <<-EOF 17 | image alpine:latest 18 | env_var 19 | EOF 20 | 21 | run dock echo 22 | [ "$status" -eq 1 ] 23 | [[ "$output" =~ "Must provide name and value for environment variable!" ]] 24 | } 25 | 26 | @test "when env_var specified name but not value it returns error" { 27 | file .dock <<-EOF 28 | image alpine:latest 29 | env_var MY_VAR 30 | EOF 31 | 32 | run dock echo 33 | [ "$status" -eq 1 ] 34 | [[ "$output" =~ "Must provide value for environment variable MY_VAR!" ]] 35 | } 36 | 37 | @test "when env_var specified it is injected into container" { 38 | file .dock <<-EOF 39 | image alpine:latest 40 | env_var MY_VAR "some value with spaces" 41 | EOF 42 | 43 | run env dock printenv MY_VAR 44 | [ "$status" -eq 0 ] 45 | [[ "$output" =~ "some value with spaces" ]] 46 | } 47 | -------------------------------------------------------------------------------- /test/configuration/image.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load ../utils 4 | 5 | setup() { 6 | destroy_all_containers 7 | original_dir="$(pwd)" 8 | cd "$(create_repo my-project)" 9 | } 10 | 11 | teardown() { 12 | cd "${original_dir}" 13 | } 14 | 15 | @test "when image not specified it returns an error" { 16 | file .dock <<-EOF 17 | # An otherwise empty configuration 18 | EOF 19 | 20 | run dock echo 21 | [ "$status" -eq 1 ] 22 | [[ "$output" =~ "Must specify either an image to run or a Dockerfile to build and run!" ]] 23 | } 24 | 25 | @test "when image specified it starts container using the image" { 26 | file .dock <<-EOF 27 | image "alpine:latest" 28 | EOF 29 | 30 | run dock cat /etc/os-release 31 | [ "$status" -eq 0 ] 32 | [[ "$output" =~ "Alpine" ]] 33 | } 34 | 35 | @test "when image and dockerfile specified it tags the built Dockerfile with image name" { 36 | file Dockerfile <<-EOF 37 | FROM alpine:latest 38 | RUN echo Hello > /etc/hello-world 39 | EOF 40 | 41 | file .dock <<-EOF 42 | dockerfile Dockerfile 43 | image "my_image:latest" 44 | EOF 45 | 46 | run dock cat /etc/hello-world 47 | [ "$status" -eq 0 ] 48 | [[ "$output" =~ "Hello" ]] 49 | 50 | dock -d nc -l -s 0.0.0.0 -p 5555 51 | run docker inspect --format {{.Config.Image}} my-project-dock 52 | docker stop my-project-dock || true 53 | [ "$status" -eq 0 ] 54 | [[ "$output" =~ "my-project:dock" ]] 55 | } 56 | -------------------------------------------------------------------------------- /test/configuration/optional_env_var.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load ../utils 4 | 5 | setup() { 6 | destroy_all_containers 7 | original_dir="$(pwd)" 8 | cd "$(create_repo)" 9 | } 10 | 11 | teardown() { 12 | cd "${original_dir}" 13 | } 14 | 15 | @test "when optional_env_var is specified but not set on host it is ignored" { 16 | file .dock <<-EOF 17 | image alpine:latest 18 | optional_env_var MY_VAR 19 | EOF 20 | 21 | run dock echo 22 | [ "$status" -eq 0 ] 23 | } 24 | 25 | @test "when optional_env_var is specified and set on host it is injected into the container" { 26 | file .dock <<-EOF 27 | image alpine:latest 28 | optional_env_var MY_VAR 29 | EOF 30 | 31 | run env MY_VAR=foo dock printenv MY_VAR 32 | [ "$status" -eq 0 ] 33 | [[ "$output" =~ "foo" ]] 34 | } 35 | 36 | @test "when optional_env_var not given an argument returns an error" { 37 | file .dock <<-EOF 38 | image alpine:latest 39 | optional_env_var 40 | EOF 41 | 42 | run dock echo 43 | [ "$status" -eq 1 ] 44 | [[ "$output" =~ "Must provide name of optional environment variable!" ]] 45 | } 46 | -------------------------------------------------------------------------------- /test/configuration/privileged.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load ../utils 4 | 5 | setup() { 6 | destroy_all_containers 7 | original_dir="$(pwd)" 8 | cd "$(create_repo my-project)" 9 | } 10 | 11 | teardown() { 12 | cd "${original_dir}" 13 | } 14 | 15 | @test "when privileged called without any arguments returns privilege status" { 16 | file .dock <<-EOF 17 | image alpine:latest 18 | if privileged; then 19 | touch is_privileged 20 | fi 21 | if ! privileged; then 22 | touch is_not_privileged 23 | fi 24 | EOF 25 | 26 | run dock echo 27 | [ "$status" -eq 0 ] 28 | [ -e is_privileged ] 29 | [ ! -e is_not_privileged ] 30 | } 31 | 32 | @test "when privileged not specified container is given privileges" { 33 | file .dock <<-EOF 34 | image alpine:latest 35 | detach true 36 | EOF 37 | 38 | dock nc -l -s 0.0.0.0 -p 5555 39 | run docker inspect --format {{.HostConfig.Privileged}} my-project-dock 40 | docker stop my-project-dock || true 41 | [ "$status" -eq 0 ] 42 | [ "$output" = true ] 43 | } 44 | 45 | @test "when privileged set to false container is not given privileges" { 46 | file .dock <<-EOF 47 | image alpine:latest 48 | detach true 49 | privileged false 50 | EOF 51 | 52 | dock nc -l -s 0.0.0.0 -p 5555 53 | run docker inspect --format {{.HostConfig.Privileged}} my-project-dock 54 | docker stop my-project-dock || true 55 | [ "$status" -eq 0 ] 56 | [ "$output" = false ] 57 | } 58 | 59 | @test "when privileged specified container is given extended privileges" { 60 | file .dock <<-EOF 61 | image alpine:latest 62 | detach true 63 | privileged true 64 | EOF 65 | 66 | dock nc -l -s 0.0.0.0 -p 5555 67 | run docker inspect --format {{.HostConfig.Privileged}} my-project-dock 68 | docker stop my-project-dock 69 | [ "$status" -eq 0 ] 70 | [ "$output" = true ] 71 | } 72 | -------------------------------------------------------------------------------- /test/configuration/publish.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load ../utils 4 | 5 | setup() { 6 | destroy_all_containers 7 | original_dir="$(pwd)" 8 | cd "$(create_repo)" 9 | } 10 | 11 | teardown() { 12 | cd "${original_dir}" 13 | } 14 | 15 | @test "when publish specified without any arguments it returns error" { 16 | file .dock <<-EOF 17 | image alpine:latest 18 | publish 19 | EOF 20 | 21 | run dock echo 22 | [ "$status" -eq 1 ] 23 | [[ "$output" =~ "Must provide port publish specification as argument!" ]] 24 | } 25 | 26 | @test "when publish specified it publishes the port according to the spec" { 27 | file .dock <<-EOF 28 | image alpine:latest 29 | detach true 30 | publish 5555:5555 31 | EOF 32 | 33 | dock nc -l -s 0.0.0.0 -p 5555 34 | run bash -c "echo | nc 127.0.0.1 5555" 35 | [ "$status" -eq 0 ] 36 | } 37 | 38 | @test "when publish specified and port already bound on host it emits a warning" { 39 | file .dock <<-EOF 40 | image alpine:latest 41 | detach true 42 | publish 5555:5555 43 | EOF 44 | 45 | nc -l 127.0.0.1 5555 & 46 | nc_pid=$! 47 | run dock nc -l -s 0.0.0.0 -p 5555 -e echo Hello world 48 | kill $nc_pid || true 49 | [ "$status" -eq 0 ] 50 | [[ "$output" =~ "Ignoring port specification 5555:5555 since another process has already bound to localhost:$host_port" ]] 51 | } 52 | -------------------------------------------------------------------------------- /test/configuration/required_env_var.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load ../utils 4 | 5 | setup() { 6 | destroy_all_containers 7 | original_dir="$(pwd)" 8 | cd "$(create_repo)" 9 | } 10 | 11 | teardown() { 12 | cd "${original_dir}" 13 | } 14 | 15 | @test "when environment variable required but not set it returns an error" { 16 | file .dock <<-EOF 17 | image alpine:latest 18 | required_env_var MY_VAR 19 | EOF 20 | 21 | run dock echo 22 | [ "$status" -eq 1 ] 23 | [[ "$output" =~ "Environment variable MY_VAR is required but not set!" ]] 24 | } 25 | 26 | @test "when required environment variable is set it is injected into the container" { 27 | file .dock <<-EOF 28 | image alpine:latest 29 | required_env_var MY_VAR 30 | EOF 31 | 32 | run env MY_VAR=foo dock printenv MY_VAR 33 | [ "$status" -eq 0 ] 34 | [[ "$output" =~ "foo" ]] 35 | } 36 | 37 | @test "when not given an argument is returns an error" { 38 | file .dock <<-EOF 39 | image alpine:latest 40 | required_env_var 41 | EOF 42 | 43 | run dock echo 44 | [ "$status" -eq 1 ] 45 | [[ "$output" =~ "Must provide name of required environment variable!" ]] 46 | } 47 | -------------------------------------------------------------------------------- /test/configuration/run_flags.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load ../utils 4 | 5 | setup() { 6 | destroy_all_containers 7 | original_dir="$(pwd)" 8 | cd "$(create_repo my-project)" 9 | } 10 | 11 | teardown() { 12 | cd "${original_dir}" 13 | } 14 | 15 | @test "when run_flags specified it adds those args to the docker build command" { 16 | file .dock <<-EOF 17 | image alpine:latest 18 | run_flags --read-only 19 | EOF 20 | 21 | run dock touch /etc/result 22 | [ "$status" -ne 0 ] 23 | [[ "$output" =~ "Read-only" ]] 24 | } 25 | 26 | @test "when run_flags specified with no arguments it returns error" { 27 | file .dock <<-EOF 28 | image alpine:latest 29 | run_flags 30 | EOF 31 | 32 | run dock echo 33 | [ "$status" -eq 1 ] 34 | [[ "$output" =~ "Must provide one or more arguments for run_flags!" ]] 35 | } 36 | -------------------------------------------------------------------------------- /test/configuration/volume.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load ../utils 4 | 5 | setup() { 6 | destroy_all_containers 7 | original_dir="$(pwd)" 8 | cd "$(create_repo)" 9 | } 10 | 11 | teardown() { 12 | cd "${original_dir}" 13 | } 14 | 15 | @test "when volume helper provided a specification it bind mounts" { 16 | echo "Some contents" > file_to_mount 17 | 18 | file .dock <<-EOF 19 | image alpine:latest 20 | volume "\$(repo_path)/file_to_mount:/etc/mounted_file" 21 | EOF 22 | 23 | run dock cat /etc/mounted_file 24 | [ "$status" -eq 0 ] 25 | [[ "$output" =~ "Some contents" ]] 26 | } 27 | 28 | @test "when volume helper is not provided a specification it fails with an error" { 29 | file .dock <<-EOF 30 | image alpine:latest 31 | volume # No argument given should result in error 32 | EOF 33 | 34 | run dock echo 35 | [ "$status" -eq 1 ] 36 | [[ "$output" =~ "Must provide volume specification!" ]] 37 | } 38 | -------------------------------------------------------------------------------- /test/configuration/workspace_path.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load ../utils 4 | 5 | setup() { 6 | destroy_all_containers 7 | original_dir="$(pwd)" 8 | cd "$(create_repo)" 9 | } 10 | 11 | teardown() { 12 | cd "${original_dir}" 13 | } 14 | 15 | @test "when workspace_path defined it mounts project at specified path" { 16 | file .dock <<-EOF 17 | image alpine:latest 18 | workspace_path /custom-workspace 19 | EOF 20 | 21 | run dock pwd 22 | [ "$status" -eq 0 ] 23 | [[ "$output" =~ "/custom-workspace" ]] 24 | } 25 | -------------------------------------------------------------------------------- /test/environment/dock_force_destroy.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load ../utils 4 | 5 | setup() { 6 | destroy_all_containers 7 | original_dir="$(pwd)" 8 | cd "$(create_repo my-project)" 9 | } 10 | 11 | teardown() { 12 | cd "${original_dir}" 13 | } 14 | 15 | @test "when DOCK_FORCE_DESTROY environment variable specified it destroys already-running container" { 16 | file .dock <<-EOF 17 | image alpine:latest 18 | EOF 19 | 20 | dock -d nc -l -s 0.0.0.0 -p 5555 21 | run env DOCK_FORCE_DESTROY=1 dock echo 22 | [ "$status" -eq 0 ] 23 | [[ "$output" =~ "Destroying container" ]] 24 | } 25 | 26 | @test "when DOCK_FORCE_DESTROY environment variable not specified and container already running it fails" { 27 | file .dock <<-EOF 28 | image alpine:latest 29 | EOF 30 | 31 | dock -d nc -l -s 0.0.0.0 -p 5555 32 | run bash -c "echo | dock echo" 33 | [ "$status" -eq 1 ] 34 | [[ "$output" =~ "already running" ]] 35 | } 36 | -------------------------------------------------------------------------------- /test/helpers/ask.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load ../utils 4 | 5 | setup() { 6 | destroy_all_containers 7 | original_dir="$(pwd)" 8 | cd "$(create_repo)" 9 | } 10 | 11 | teardown() { 12 | cd "${original_dir}" 13 | } 14 | 15 | @test "returns default answers when not run in an interactive context" { 16 | file .dock <<-EOF 17 | image alpine:latest 18 | 19 | ask "Question 1" default_answer_1 answer_1 20 | ask "Question 2" default_answer_2 answer_2 21 | echo "\${answer_1}" > answer_1 22 | echo "\${answer_2}" > answer_2 23 | EOF 24 | 25 | echo | dock echo 26 | [ "$(cat answer_1)" = default_answer_1 ] 27 | [ "$(cat answer_2)" = default_answer_2 ] 28 | } 29 | 30 | @test "prompts user on standard error for input when run in interactive context" { 31 | file .dock <<-EOF 32 | image alpine:latest 33 | 34 | ask "Question 1" default_answer_1 answer_1 35 | ask "Question 2" default_answer_2 answer_2 36 | echo "\${answer_1}" > answer_1 37 | echo "\${answer_2}" > answer_2 38 | EOF 39 | 40 | file answers <<-EOF 41 | custom_answer_1 42 | custom_answer_2 43 | EOF 44 | 45 | # Since `script` returns when all input has been read, we need to append an 46 | # endless stream of "y" using `yes` onto the end of our answers so that 47 | # standard input doesn' close until `dock` has finished executing. 48 | yes | cat answers - | script -c "dock echo" /dev/null > output 49 | 50 | # Check that prompts are shown 51 | [[ "$(cat output)" =~ "Question 1" ]] 52 | [[ "$(cat output)" =~ "Question 2" ]] 53 | # ...and the default answers 54 | [[ "$(cat output)" =~ "default_answer_1" ]] 55 | [[ "$(cat output)" =~ "default_answer_2" ]] 56 | } 57 | 58 | # TODO: This test seems to consistently hang and timeout (as of 05/10/17) on master; despite 59 | # having passed in the past (especially with respect to the last change which was committed (722ef38). 60 | # The cause of the failure is currently unknown and technically should be unrelated to recent changes. 61 | # Re-enable once the cause has been identified and resolved. 62 | #@test "returns user's answers when run in an interactive context" { 63 | # file .dock <<-EOF 64 | #image alpine:latest 65 | # 66 | #ask "Question 1" default_answer_1 answer_1 67 | #ask "Question 2" default_answer_2 answer_2 68 | #echo "\${answer_1}" > answer_1 69 | #echo "\${answer_2}" > answer_2 70 | #EOF 71 | # 72 | # file answers <<-EOF 73 | #custom_answer_1 74 | #custom_answer_2 75 | #EOF 76 | # 77 | # # Since `script` returns when all input has been read, we need to append an 78 | # # endless stream of "y" using `yes` onto the end of our answers so that 79 | # # standard input doesn' close until `dock` has finished executing. 80 | # yes | cat answers - | script -c dock /dev/null 81 | # 82 | # [ "$(cat answer_1)" = custom_answer_1 ] 83 | # [ "$(cat answer_2)" = custom_answer_2 ] 84 | #} 85 | -------------------------------------------------------------------------------- /test/helpers/container_hostname.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load ../utils 4 | 5 | project_name=my-project 6 | 7 | setup() { 8 | destroy_all_containers 9 | original_dir="$(pwd)" 10 | cd "$(create_repo ${project_name})" 11 | } 12 | 13 | teardown() { 14 | cd "${original_dir}" 15 | } 16 | 17 | @test "returns the hostname of the container" { 18 | file .dock <<-EOF 19 | image alpine:latest 20 | container_hostname my-hostname 21 | echo "\$(container_hostname)" > hostname 22 | EOF 23 | 24 | run dock echo 25 | [ "$status" -eq 0 ] 26 | [ "$(cat hostname)" = my-hostname ] 27 | } 28 | 29 | @test "returns the hostname defined by container_hostname option" { 30 | file .dock <<-EOF 31 | image alpine:latest 32 | container_hostname my-custom-name 33 | echo "\$(container_hostname)" > hostname 34 | EOF 35 | 36 | run dock echo 37 | [ "$status" -eq 0 ] 38 | [ "$(cat hostname)" = my-custom-name ] 39 | } 40 | -------------------------------------------------------------------------------- /test/helpers/container_name.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load ../utils 4 | 5 | project_name=my-project 6 | 7 | setup() { 8 | destroy_all_containers 9 | original_dir="$(pwd)" 10 | cd "$(create_repo ${project_name})" 11 | } 12 | 13 | teardown() { 14 | cd "${original_dir}" 15 | } 16 | 17 | @test "returns the name of the container" { 18 | file .dock <<-EOF 19 | image alpine:latest 20 | echo "\$(container_name)" > container_name 21 | EOF 22 | 23 | run dock echo 24 | [ "$status" -eq 0 ] 25 | [ "$(cat container_name)" = my-project-dock ] 26 | } 27 | 28 | @test "returns the name defined by container_name option" { 29 | file .dock <<-EOF 30 | image alpine:latest 31 | container_name my-custom-name 32 | echo "\$(container_name)" > container_name 33 | EOF 34 | 35 | run dock echo 36 | [ "$status" -eq 0 ] 37 | [ "$(cat container_name)" = my-custom-name ] 38 | } 39 | -------------------------------------------------------------------------------- /test/helpers/group_id.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load ../utils 4 | 5 | setup() { 6 | destroy_all_containers 7 | original_dir="$(pwd)" 8 | cd "$(create_repo)" 9 | } 10 | 11 | teardown() { 12 | cd "${original_dir}" 13 | } 14 | 15 | @test "returns the group ID of the user who ran Dock" { 16 | file .dock <<-EOF 17 | image alpine:latest 18 | echo "\$(group_id)" > group_id 19 | EOF 20 | 21 | run dock echo 22 | [ "$status" -eq 0 ] 23 | [ "$(cat group_id)" = "$(id -g)" ] 24 | } 25 | -------------------------------------------------------------------------------- /test/helpers/interactive.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load ../utils 4 | 5 | setup() { 6 | destroy_all_containers 7 | original_dir="$(pwd)" 8 | cd "$(create_repo)" 9 | } 10 | 11 | teardown() { 12 | cd "${original_dir}" 13 | } 14 | 15 | @test "returns true when standard input is a TTY" { 16 | file .dock <<-EOF 17 | image alpine:latest 18 | 19 | if interactive; then 20 | touch is_interactive 21 | fi 22 | if ! interactive; then 23 | touch is_not_interactive 24 | fi 25 | EOF 26 | 27 | run script --return -c "dock echo" /dev/null 28 | [ "$status" -eq 0 ] 29 | [ -e is_interactive ] 30 | [ ! -e is_not_interactive ] 31 | } 32 | 33 | @test "returns false when standard input is not a TTY" { 34 | file .dock <<-EOF 35 | image alpine:latest 36 | 37 | if interactive; then 38 | touch is_interactive 39 | fi 40 | if ! interactive; then 41 | touch is_not_interactive 42 | fi 43 | EOF 44 | 45 | echo | dock echo 46 | [ ! -e is_interactive ] 47 | [ -e is_not_interactive ] 48 | } 49 | -------------------------------------------------------------------------------- /test/helpers/linux.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load ../utils 4 | 5 | setup() { 6 | destroy_all_containers 7 | original_dir="$(pwd)" 8 | cd "$(create_repo)" 9 | } 10 | 11 | teardown() { 12 | cd "${original_dir}" 13 | } 14 | 15 | # This will always return true since we're running inside the Dock container, 16 | # which is based on CentOS. Far from perfect but "good enough" 17 | @test "returns true if the OS is Linux" { 18 | file .dock <<-EOF 19 | image alpine:latest 20 | 21 | if linux; then 22 | touch is_linux 23 | fi 24 | if ! linux; then 25 | touch is_not_linux 26 | fi 27 | EOF 28 | 29 | run dock echo 30 | [ "$status" -eq 0 ] 31 | [ -e is_linux ] 32 | [ ! -e is_not_linux ] 33 | } 34 | -------------------------------------------------------------------------------- /test/helpers/mac_os.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load ../utils 4 | 5 | setup() { 6 | destroy_all_containers 7 | original_dir="$(pwd)" 8 | cd "$(create_repo)" 9 | } 10 | 11 | teardown() { 12 | cd "${original_dir}" 13 | } 14 | 15 | # This will always return false since we're running inside the Dock container, 16 | # which is based on CentOS. Far from perfect but "good enough" 17 | @test "returns whether the OS is macOS" { 18 | file .dock <<-EOF 19 | image alpine:latest 20 | 21 | if mac_os; then 22 | touch is_mac_os 23 | fi 24 | if ! mac_os; then 25 | touch is_not_mac_os 26 | fi 27 | EOF 28 | 29 | run dock echo 30 | [ "$status" -eq 0 ] 31 | [ -e is_not_mac_os ] 32 | [ ! -e is_mac_os ] 33 | } 34 | -------------------------------------------------------------------------------- /test/helpers/repo_path.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load ../utils 4 | 5 | setup() { 6 | destroy_all_containers 7 | original_dir="$(pwd)" 8 | actual_repo_path="$(create_repo)" 9 | cd "${actual_repo_path}" 10 | } 11 | 12 | teardown() { 13 | cd "${original_dir}" 14 | } 15 | 16 | @test "returns absolute path to the repo" { 17 | file .dock <<-EOF 18 | image alpine:latest 19 | echo "\$(repo_path)" > repo_path 20 | EOF 21 | 22 | run dock echo 23 | [ "$status" -eq 0 ] 24 | [ "$(cat repo_path)" = "${actual_repo_path}" ] 25 | } 26 | -------------------------------------------------------------------------------- /test/helpers/user_id.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load ../utils 4 | 5 | setup() { 6 | destroy_all_containers 7 | original_dir="$(pwd)" 8 | cd "$(create_repo)" 9 | } 10 | 11 | teardown() { 12 | cd "${original_dir}" 13 | } 14 | 15 | @test "returns the user ID of the user who ran Dock" { 16 | file .dock <<-EOF 17 | image alpine:latest 18 | echo "\$(user_id)" > user_id 19 | EOF 20 | 21 | run dock echo 22 | [ "$status" -eq 0 ] 23 | [ "$(cat user_id)" = "$(id -u)" ] 24 | } 25 | -------------------------------------------------------------------------------- /test/helpers/workspace_path.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load ../utils 4 | 5 | setup() { 6 | destroy_all_containers 7 | original_dir="$(pwd)" 8 | cd "$(create_repo)" 9 | } 10 | 11 | teardown() { 12 | cd "${original_dir}" 13 | } 14 | 15 | @test "returns absolute path to the directory mounted in container" { 16 | file .dock <<-EOF 17 | image alpine:latest 18 | echo "\$(workspace_path)" > workspace_path 19 | EOF 20 | 21 | run dock echo 22 | [ "$status" -eq 0 ] 23 | [ "$(cat workspace_path)" = /workspace ] 24 | } 25 | 26 | @test "returns absolute path defined by workspace_path option" { 27 | file .dock <<-EOF 28 | image alpine:latest 29 | workspace_path /custom-workspace 30 | echo "\$(workspace_path)" > workspace_path 31 | EOF 32 | 33 | run dock echo 34 | [ "$status" -eq 0 ] 35 | [ "$(cat workspace_path)" = /custom-workspace ] 36 | } 37 | -------------------------------------------------------------------------------- /test/options/attach.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load ../utils 4 | 5 | project_name=my-project 6 | 7 | setup() { 8 | destroy_all_containers 9 | original_dir="$(pwd)" 10 | cd "$(create_repo ${project_name})" 11 | } 12 | 13 | teardown() { 14 | cd "${original_dir}" 15 | } 16 | 17 | @test "attaching when container is already running" { 18 | docker run --detach --interactive --name "${project_name}-dock" alpine:latest \ 19 | sleep 5 20 | run dock -a 21 | # 137 status means we were killed by the container exiting 22 | [ "$status" -eq 137 ] 23 | } 24 | 25 | @test "attaching when container exists but is not running" { 26 | docker run --name "${project_name}-dock" alpine:latest echo 27 | run dock -a 28 | [ "$status" -eq 1 ] 29 | [[ "${lines[0]}" =~ "Container ${project_name}-dock exists but is not running, so you can't attach" ]] 30 | } 31 | 32 | @test "attaching when no container exists" { 33 | run dock -a 34 | [ "$status" -eq 1 ] 35 | [[ "${lines[0]}" =~ "No container named ${project_name}-dock is currently running." ]] 36 | [[ "${lines[1]}" =~ "You must start the container first before you can attach!" ]] 37 | } 38 | -------------------------------------------------------------------------------- /test/options/config.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load ../utils 4 | 5 | project_name=my-project 6 | 7 | setup() { 8 | destroy_all_containers 9 | original_dir="$(pwd)" 10 | cd "$(create_repo ${project_name})" 11 | } 12 | 13 | teardown() { 14 | cd "${original_dir}" 15 | } 16 | 17 | @test "specifying an explicit config file" { 18 | file .dock <<-EOF 19 | image alpine:latest 20 | default_command exit 1 21 | EOF 22 | 23 | file .other-dock <<-EOF 24 | image alpine:latest 25 | default_command echo Hello 26 | EOF 27 | 28 | run dock -c .other-dock 29 | [ "$status" -eq 0 ] 30 | [[ "$output" =~ "Hello" ]] 31 | } 32 | 33 | @test "specifying a non-existent config file" { 34 | run dock -c .other-dock echo 35 | [ "$status" -eq 1 ] 36 | [[ "$output" =~ "Dock configuration file '.other-dock' does not exist!" ]] 37 | } 38 | -------------------------------------------------------------------------------- /test/options/detach.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load ../utils 4 | 5 | project_name=my-project 6 | 7 | setup() { 8 | destroy_all_containers 9 | original_dir="$(pwd)" 10 | cd "$(create_repo ${project_name})" 11 | echo "image=alpine:latest" > .dock 12 | } 13 | 14 | teardown() { 15 | cd "${original_dir}" 16 | } 17 | 18 | @test "running Dock container as detached" { 19 | run dock -d sh 20 | [ "$status" -eq 0 ] 21 | container_running "${project_name}-dock" 22 | } 23 | -------------------------------------------------------------------------------- /test/options/extend.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load ../utils 4 | 5 | project_name=my-project 6 | 7 | setup() { 8 | destroy_all_containers 9 | original_dir="$(pwd)" 10 | cd "$(create_repo ${project_name})" 11 | } 12 | 13 | teardown() { 14 | cd "${original_dir}" 15 | } 16 | 17 | @test "configuration labels are added to Dock container during extension" { 18 | file Dockerfile <<-EOF 19 | FROM alpine:latest 20 | RUN apk update && apk add jq && rm -rf /var/cache/apk/* 21 | EOF 22 | 23 | file .dock <<-EOF 24 | dockerfile Dockerfile 25 | default_command sh 26 | EOF 27 | 28 | file docker-compose.yml <<-EOF 29 | version: '2' 30 | EOF 31 | 32 | run dock -e test 33 | 34 | [ "$status" -eq 0 ] 35 | # verify labels have been set correctly 36 | labels="$(get_labels test)" 37 | echo "$labels" | grep compose.my-project 38 | echo "$labels" | grep dock.my-project 39 | } 40 | 41 | @test "compose configuration label is not added to Dock container if compose file does not exist" { 42 | file Dockerfile <<-EOF 43 | FROM alpine:latest 44 | RUN apk update && apk add jq && rm -rf /var/cache/apk/* 45 | EOF 46 | 47 | file .dock <<-EOF 48 | dockerfile Dockerfile 49 | default_command sh 50 | EOF 51 | 52 | run dock -e test 53 | 54 | [ "$status" -eq 0 ] 55 | labels="$(get_labels test)" 56 | # ensure compose construction label for project is NOT set 57 | [[ "${lines[1]}" =~ "Unable to locate a docker-compose.yml file" ]] 58 | echo "$labels" | grep -v compose.my-project 59 | echo "$labels" | grep dock.my-project 60 | } 61 | 62 | @test "workspace dir is set as repo root of project during extension" { 63 | file Dockerfile <<-EOF 64 | FROM alpine:latest 65 | RUN apk update && apk add jq && rm -rf /var/cache/apk/* 66 | EOF 67 | 68 | file .dock <<-EOF 69 | dockerfile Dockerfile 70 | default_command sh 71 | workspace_path /custom-workspace # .dock setting should be overridden 72 | EOF 73 | 74 | file docker-compose.yml <<-EOF 75 | version: '2' 76 | EOF 77 | 78 | run dock -e test 79 | 80 | [ "$status" -eq 0 ] 81 | [[ "$(dock exec test pwd)" =~ "$(original_dir)" ]] 82 | } 83 | 84 | @test "extending a project overrides previous Dock configuration labels for project" { 85 | file Dockerfile <<-EOF 86 | FROM alpine:latest 87 | RUN apk update && apk add jq && rm -rf /var/cache/apk/* 88 | EOF 89 | 90 | file .dock <<-EOF 91 | dockerfile Dockerfile 92 | default_command sh 93 | EOF 94 | 95 | file docker-compose.yml <<-EOF 96 | version: '2' 97 | EOF 98 | 99 | docker run --name test -d --label compose.my-project=foo --label dock.my-project=bar \ 100 | alpine:latest sh 101 | 102 | run dock -e test 103 | 104 | [ "$status" -eq 0 ] 105 | # ensure project configuration labels previously set are overriden appropriately 106 | labels="$(get_labels test)" 107 | [ "$(echo "$labels" | grep -c foo)" -eq 0 ] 108 | [ "$(echo "$labels" | grep -c bar)" -eq 0 ] 109 | [ "$(echo "$labels" | grep -c ${repo_path}/docker-compose.yml)" -eq 1 ] 110 | [ "$(echo "$labels" | grep -c ${repo_path}/.dock)" -eq 1 ] 111 | } 112 | 113 | @test "extending a non-existent Dock container successfully" { 114 | file Dockerfile <<-EOF 115 | FROM alpine:latest 116 | RUN apk update && apk add jq && rm -rf /var/cache/apk/* 117 | EOF 118 | 119 | file .dock <<-EOF 120 | dockerfile Dockerfile 121 | default_command sh 122 | publish 7777:7777 123 | EOF 124 | 125 | file docker-compose.yml <<-EOF 126 | version: '2' 127 | EOF 128 | 129 | run dock -e test 130 | 131 | [ "$status" -eq 0 ] 132 | # verify image for Dock env has been created 133 | docker images --format '{{.Repository}}:{{.Tag}}'| grep test:dock 134 | # ensure Dock env container is left running 135 | container_running test 136 | # verify extension project configuration labels are set accordingly 137 | labels="$(get_labels test)" 138 | echo "$labels" | grep compose.my-project 139 | echo "$labels" | grep dock.my-project 140 | # verify projects label is updated 141 | echo "$labels" | grep projects 142 | echo "$labels" | grep my-project 143 | # verify project ports are published 144 | docker port test 7777 145 | } 146 | 147 | @test "extending an existing Dock container successfully" { 148 | file Dockerfile <<-EOF 149 | FROM alpine:latest 150 | RUN apk update && apk add jq && rm -rf /var/cache/apk/* 151 | EOF 152 | 153 | echo "mytesting" > /tmp/myfile 154 | file .dock <<-EOF 155 | dockerfile Dockerfile 156 | volume "/tmp/myfile:/myprojectrepo/myfile" 157 | publish 8888:8888 158 | default_command sh 159 | EOF 160 | 161 | file docker-compose.yml <<-EOF 162 | version: '2' 163 | EOF 164 | 165 | echo "atesting" > /tmp/afile 166 | docker run --name test -d --volume /tmp/afile:/aprojectrepo/afile --label compose.aproject=foo \ 167 | --label dock.aproject=bar --label projects=aproject alpine:latest sh 168 | docker commit test test:dock 169 | 170 | dock -e test 171 | run docker exec test cat /myprojectrepo/myfile 172 | 173 | [ "$status" -eq 0 ] 174 | # verify proper volume mounting of extension project and preservation of existing project mounts 175 | [[ "$output" =~ "mytesting" ]] 176 | [[ "$(docker exec test cat /aprojectrepo/afile)" =~ "atesting" ]] 177 | # verify extension project configuration labels are set accordingly 178 | labels="$(get_labels test)" 179 | echo "$labels" | grep compose.my-project 180 | echo "$labels" | grep dock.my-project 181 | # verify projects label is updated 182 | echo "$labels" | grep projects 183 | echo "$labels" | grep my-project 184 | echo "$labels" | grep aproject 185 | # verify project ports are published 186 | docker port test 8888 187 | } 188 | 189 | @test "startup_services labels are set when specified by project configuration" { 190 | file Dockerfile <<-EOF 191 | FROM alpine:latest 192 | RUN apk update && apk add jq && rm -rf /var/cache/apk/* 193 | EOF 194 | 195 | file .dock <<-EOF 196 | dockerfile Dockerfile 197 | startup_services 'service backend' 198 | default_command sh 199 | EOF 200 | 201 | file docker-compose.yml <<-EOF 202 | version: '2' 203 | EOF 204 | 205 | run dock -e test 206 | 207 | [ "$status" -eq 0 ] 208 | # verify startup_services label has been applied 209 | labels="$(get_labels test)" 210 | [ "$(echo "$labels" | grep -c startup_services)" -eq 1 ] 211 | echo "$labels" | grep service 212 | echo "$labels" | grep backend 213 | } 214 | 215 | @test "duplicate startup_services label values are filtered during extension" { 216 | file Dockerfile <<-EOF 217 | FROM alpine:latest 218 | RUN apk update && apk add jq && rm -rf /var/cache/apk/* 219 | EOF 220 | 221 | file .dock <<-EOF 222 | dockerfile Dockerfile 223 | startup_services 'another_service backend another_backend' 224 | default_command sh 225 | EOF 226 | 227 | file docker-compose.yml <<-EOF 228 | version: '2' 229 | EOF 230 | 231 | docker run --name test -d --label startup_services='service backend' alpine:latest sh 232 | run dock -e test 233 | 234 | [ "$status" -eq 0 ] 235 | # verify duplicated startup_services labels are filtered accordingly 236 | labels="$(get_labels test)" 237 | [ "$(echo "$labels" | grep -c startup_services)" -eq 1 ] 238 | [ "$(echo "$labels" | grep -c service)" -eq 1 ] 239 | [ "$(echo "$labels" | grep -c another_service)" -eq 1 ] 240 | [ "$(echo "$labels" | grep -c backend)" -eq 1 ] 241 | [ "$(echo "$labels" | grep -c another_backend)" -eq 1 ] 242 | } 243 | -------------------------------------------------------------------------------- /test/options/force.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load ../utils 4 | 5 | project_name=my-project 6 | 7 | setup() { 8 | destroy_all_containers 9 | original_dir="$(pwd)" 10 | cd "$(create_repo ${project_name})" 11 | echo "image alpine:latest" > .dock 12 | } 13 | 14 | teardown() { 15 | cd "${original_dir}" 16 | } 17 | 18 | @test "forcibly removing existing Dock container" { 19 | container_id="$(dock -d sh)" 20 | run dock -f echo 21 | [ "$status" -eq 0 ] 22 | ! container_running "$container_id" 23 | } 24 | -------------------------------------------------------------------------------- /test/options/help.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load ../utils 4 | 5 | setup() { 6 | destroy_all_containers 7 | original_dir="$(pwd)" 8 | cd "$(create_repo my-project)" 9 | } 10 | 11 | teardown() { 12 | cd "${original_dir}" 13 | } 14 | 15 | @test "when -h flag specified it displays help documentation" { 16 | run dock -h 17 | echo "$output" 18 | [ "$status" -eq 0 ] 19 | [[ "$output" =~ "dock is a tool" ]] 20 | [[ "$output" =~ "Usage: dock [options] [command]" ]] 21 | } 22 | -------------------------------------------------------------------------------- /test/options/quiet.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load ../utils 4 | 5 | setup() { 6 | destroy_all_containers 7 | original_dir="$(pwd)" 8 | cd "$(create_repo my-project)" 9 | } 10 | 11 | teardown() { 12 | cd "${original_dir}" 13 | } 14 | 15 | @test "when quiet flag specified no output from Dock is emitted" { 16 | file .dock <<-EOF 17 | image alpine:latest 18 | EOF 19 | 20 | run dock -q echo -n Hello world 21 | [ "$status" -eq 0 ] 22 | [ "$output" = "Hello world" ] 23 | } 24 | 25 | @test "when quiet flag specified no output from Dockerfile build is emitted" { 26 | file Dockerfile <<-EOF 27 | FROM alpine:latest 28 | RUN touch /etc/something 29 | EOF 30 | 31 | file .dock <<-EOF 32 | dockerfile Dockerfile 33 | EOF 34 | 35 | run dock -q echo -n Hello world 36 | echo "$output" 37 | [ "$status" -eq 0 ] 38 | [ "$output" = "Hello world" ] 39 | } 40 | -------------------------------------------------------------------------------- /test/options/verbose_version.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load ../utils 4 | 5 | setup() { 6 | destroy_all_containers 7 | original_dir="$(pwd)" 8 | cd "$(create_repo my-project)" 9 | } 10 | 11 | teardown() { 12 | cd "${original_dir}" 13 | } 14 | 15 | @test "when -V flag specified it displays the Dock version" { 16 | run dock -V 17 | [ "$status" -eq 0 ] 18 | [[ "$output" =~ Dock:\ +[0-9]+\.[0-9]+\.[0-9]+ ]] 19 | } 20 | 21 | @test "when -V flag specified it displays the Docker version" { 22 | run dock -V 23 | [ "$status" -eq 0 ] 24 | [[ "$output" =~ Docker:\ +Docker\ version\ [0-9]+\.[0-9]+\.[0-9]+ ]] 25 | } 26 | 27 | @test "when -V flag specified it displays the Bash version" { 28 | run dock -V 29 | [ "$status" -eq 0 ] 30 | [[ "$output" =~ Bash:\ +"$BASH_VERSION" ]] 31 | } 32 | 33 | @test "when -V flag specified it displays the OS version" { 34 | run dock -V 35 | [ "$status" -eq 0 ] 36 | [[ "$output" =~ OS:\ +$(uname -a) ]] 37 | } 38 | -------------------------------------------------------------------------------- /test/options/version.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load ../utils 4 | 5 | setup() { 6 | destroy_all_containers 7 | original_dir="$(pwd)" 8 | cd "$(create_repo my-project)" 9 | } 10 | 11 | teardown() { 12 | cd "${original_dir}" 13 | } 14 | 15 | @test "when -v flag specified it displays the Dock version" { 16 | run dock -v 17 | [ "$status" -eq 0 ] 18 | [[ "$output" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]] 19 | } 20 | -------------------------------------------------------------------------------- /test/utils.bash: -------------------------------------------------------------------------------- 1 | # Defines helper functions used across a variety of tests. 2 | 3 | create_repo() { 4 | local dir_name="${1:-}" 5 | if [ -z "${dir_name}" ]; then 6 | repo_path="$(mktemp --directory)" 7 | else 8 | repo_path="$(mktemp --directory)/${dir_name}" 9 | fi 10 | 11 | mkdir -p "${repo_path}" 12 | git init "${repo_path}" >/dev/null 2>&1 13 | 14 | echo ${repo_path} 15 | } 16 | 17 | destroy_all_containers() { 18 | docker ps -aq | xargs --no-run-if-empty docker rm --force 19 | } 20 | 21 | container_running() { 22 | [ "$(docker inspect --format={{.State.Status}} "$1")" = running ] 23 | } 24 | 25 | get_labels() { 26 | echo $(docker inspect --format='{{json .Config.Labels}}' "$1") 27 | } 28 | 29 | file() { 30 | local file=$1 31 | 32 | # `file` will be passed file contents via STDIN 33 | while read data; do 34 | echo "$data" >> "$file" 35 | done 36 | } 37 | --------------------------------------------------------------------------------