├── .gitignore ├── .isort.cfg ├── .travis.yml ├── CHANGES.rst ├── DOCKHAND_CHANGES.rst ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── NOTICE ├── README.rst ├── docs ├── .gitignore-gh-pages ├── Dockerfile ├── Gemfile ├── Gemfile.lock ├── _config.yml ├── imgs │ └── image-graph.png ├── index.md └── motivation.md ├── setup.cfg ├── setup.py ├── shipwright ├── __init__.py ├── __main__.py ├── _lib │ ├── __init__.py │ ├── base.py │ ├── build.py │ ├── cache.py │ ├── cli.py │ ├── colors.py │ ├── compat.py │ ├── dependencies.py │ ├── docker.py │ ├── image.py │ ├── msg.py │ ├── push.py │ ├── registry.py │ ├── source_control.py │ ├── tar.py │ ├── targets.py │ └── zipper.py ├── exceptions.py └── targets.py ├── tests ├── conftest.py ├── integration │ ├── __init__.py │ ├── examples │ │ ├── failing-build │ │ │ ├── .shipwright.json │ │ │ ├── base │ │ │ │ ├── Dockerfile │ │ │ │ └── base.txt │ │ │ ├── crashy-from │ │ │ │ └── Dockerfile │ │ │ ├── crashy-run │ │ │ │ └── Dockerfile │ │ │ └── works │ │ │ │ └── Dockerfile │ │ ├── multi-dockerfile │ │ │ ├── .shipwright.json │ │ │ ├── base │ │ │ │ ├── Dockerfile │ │ │ │ └── base.txt │ │ │ └── service1 │ │ │ │ ├── Dockerfile │ │ │ │ ├── Dockerfile-dev │ │ │ │ ├── src │ │ │ │ └── foo.js │ │ │ │ └── test │ │ │ │ └── test_foo.js │ │ ├── shipwright-localhost-sample │ │ │ ├── .shipwright.json │ │ │ ├── base │ │ │ │ ├── Dockerfile │ │ │ │ └── base.txt │ │ │ ├── service1 │ │ │ │ └── Dockerfile │ │ │ └── shared │ │ │ │ └── Dockerfile │ │ └── shipwright-sample │ │ │ ├── .shipwright.json │ │ │ ├── base │ │ │ ├── Dockerfile │ │ │ └── base.txt │ │ │ ├── service1 │ │ │ └── Dockerfile │ │ │ └── shared │ │ │ └── Dockerfile │ ├── test_docker_builds.py │ ├── test_docker_push.py │ ├── test_git.py │ ├── test_images.py │ ├── test_targets.py │ └── utils.py ├── test_cli.py ├── test_dependencies.py ├── test_switch.py └── test_tar.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # Installer logs 26 | pip-log.txt 27 | pip-delete-this-directory.txt 28 | 29 | # Unit test / coverage reports 30 | htmlcov/ 31 | .tox/ 32 | .coverage 33 | .cache 34 | nosetests.xml 35 | coverage.xml 36 | 37 | # Translations 38 | *.mo 39 | 40 | # Mr Developer 41 | .mr.developer.cfg 42 | .project 43 | .pydevproject 44 | 45 | # Rope 46 | .ropeproject 47 | 48 | # Django stuff: 49 | *.log 50 | *.pot 51 | 52 | # Sphinx documentation 53 | docs/_build/ 54 | 55 | -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | line_length=80 3 | indent=' ' 4 | multi_line_output=5 5 | balanced_wrapping=True 6 | include_trailing_comma=True 7 | known_first_party=shipwright 8 | not_skip=__init__.py 9 | add_imports=from __future__ import absolute_import 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | services: 3 | - docker 4 | language: python 5 | python: "3.5" 6 | matrix: 7 | include: 8 | env: TOX_ENV=py36 9 | python: "3.6" 10 | env: 11 | - TOX_ENV=py27 12 | - TOX_ENV=py34 13 | - TOX_ENV=py35 14 | - TOX_ENV=lint 15 | - TOX_ENV=isort 16 | install: pip install tox 17 | script: tox -e $TOX_ENV 18 | cache: pip 19 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | 0.9.1 (unreleased) 2 | ------------------ 3 | 4 | - Nothing changed yet. 5 | 6 | 7 | 0.9.0 (2017-06-29) 8 | ------------------ 9 | 10 | - Add better fast-alias error message for missing manifest. 11 | (`Issue #103 `_). 12 | - Retry on error during docker push. 13 | (`Issue #104 `_). 14 | - Pull parent images before build to avoid problems with 15 | docker-py build not sending credentials. 16 | (`Issue #102 `_). 17 | - Push and tag images as soon as they are built. 18 | (`Issue #101 `_). 19 | - Base the cache key on the globs in Docker COPY/ADD commands. 20 | (`Issue #98 `_). 21 | 22 | 23 | 0.8.0 (2017-06-08) 24 | ------------------ 25 | 26 | - Add proper stack traces for direct registry errors. 27 | (`Issue #93 `_). 28 | - Handle differences between docker over TCP and Unix socket. 29 | (`Issue #96 `_). 30 | - Mark every internal package as private to allow exporting 31 | of select parts of shipwright publicly. 32 | (`Issue #99 `_). 33 | - Create shipwright.targets.targets function to list available 34 | docker targets in a repo programaticaly. 35 | (`Issue #100 `_). 36 | 37 | 38 | 0.7.0 (2017-01-13) 39 | ------------------ 40 | 41 | - Depend on docker>=2.0.1. 42 | (`Issue #91 `_). 43 | 44 | 45 | 0.6.6 (2017-01-13) 46 | ------------------ 47 | 48 | - Exprimental --registry-login cache flag to skip creation of already built 49 | images and speed up tagging. Feature not subject to semver. 50 | (`Issue #89 `_). 51 | 52 | 0.6.5 (2017-01-08) 53 | ------------------ 54 | 55 | - Fix changelog. 56 | 57 | 58 | 0.6.4 (2017-01-08) 59 | ------------------ 60 | 61 | - Add images command for creating docker save. 62 | (`Issue #88 `_). 63 | 64 | 65 | 0.6.3 (2016-08-24) 66 | ------------------ 67 | 68 | - Push images to the registry in parallel. 69 | (`Issue #82 `_). 70 | 71 | 72 | 0.6.2 (2016-08-23) 73 | ------------------ 74 | 75 | - Also push image target ref so that --pull-cache can pull them. 76 | (`Issue #81 `_). 77 | 78 | 79 | 0.6.1 (2016-08-23) 80 | ------------------ 81 | 82 | - Warn on --pull-cache errors 83 | (`Issue #80 `_). 84 | 85 | 86 | 0.6.0 (2016-08-22) 87 | ------------------ 88 | 89 | - Add --pull-cache to pull images from repository before building. 90 | (`Issue #49 `_). 91 | 92 | 93 | 0.5.0 (2016-08-19) 94 | ------------------ 95 | 96 | - Add --dirty to build from working tree, even when uncommitted and untracked changes exist. 97 | (`Issue #74 `_). 98 | Thanks `James Pickering `_! 99 | - Ignore images without RepoTags when gathering built_tags to fix a crash 100 | caused by docker images pulled via RepoDigest. 101 | (`Issue #77 `_). 102 | Thanks `kgpayne `_! 103 | 104 | 105 | 0.4.2 (2016-06-16) 106 | ------------------ 107 | 108 | - Correct naming, shipwright builds docker images. 109 | (`Issue #71 `_) 110 | - Allow building with a detached HEAD 111 | (`Issue #72 `_) 112 | 113 | 114 | 0.4.1 (2016-06-15) 115 | ------------------ 116 | 117 | - Fix push crash. (`Issue #70 `_) 118 | 119 | 120 | 0.4.0 (2016-06-13) 121 | ------------------ 122 | 123 | - Isolate all git functionality, so as to create pluggable Source Control wrappers. 124 | - More efficient required build detection. (`Issue #63 `_) 125 | - Isolate all zipper usage, vendor zipper library. 126 | 127 | 0.2.2 (2015-01-07) 128 | ------------------ 129 | 130 | - Fix bug missing ``tls`` when communicating with docker over a unix 131 | socket. 132 | 133 | 0.2.1 (2015-01-01) 134 | ------------------ 135 | 136 | - Force tag to support docker 1.4.1 137 | - Requries docker-py >= 0.6 138 | - Added ``assert_hostname`` as an option to ``.shipwright.json`` 139 | - Added command line option ``--x-assert-hostname`` to disable hostname 140 | checking when TLS is used. Useful for boot2docker 141 | 142 | 0.2.0 (2014-12-31) 143 | ------------------ 144 | 145 | - Added ``shipwright push`` and ``shipwright purge`` 146 | - Added support for specifiers ``-u``, ``-d``, ``-e`` and ``-x`` 147 | 148 | 0.1.0 (2014-09-10) 149 | ------------------ 150 | 151 | - Build and tag containers 152 | - Moved config to ``.shipwright.json`` 153 | -------------------------------------------------------------------------------- /DOCKHAND_CHANGES.rst: -------------------------------------------------------------------------------- 1 | 0.3.3 (unreleased) 2 | ------------------ 3 | 4 | - Isolate all git functionality, so as to create pluggable Source Control wrappers. 5 | 6 | 7 | 0.3.2 (2016-06-06) 8 | ------------------ 9 | 10 | - Fix issue when images are built, but not in git. 11 | 12 | 0.3.0 (2016-06-06) 13 | ------------------ 14 | 15 | - Support .dockerignore files on Py3k 16 | - shipwright 'build' sub-command is no-longer optional 17 | - Support extra tags with '-t' 18 | - Continue the build on failure, defer sys.exit until the end of the build. 19 | - Support shor names in TARGETS. 20 | 21 | 22 | 0.2.0 (2016-05-19) 23 | ------------------ 24 | 25 | - Removed @curry decorator. 26 | - Removed shipwright.version module. 27 | - Removed purge command. 28 | - Fixed various Python 3 bugs. 29 | 30 | 31 | 0.1.0 (2016-05-13) 32 | ------------------ 33 | 34 | - First version of dockhand that can be installed from PyPI 35 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:2.7-onbuild 2 | RUN cd /usr/src/app && python setup.py develop 3 | WORKDIR /code 4 | #ENTRYPOINT shipwright 5 | CMD shipwright -------------------------------------------------------------------------------- /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 {yyyy} {name of copyright owner} 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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGES.rst 2 | include README.rst 3 | include LICENSE 4 | include NOTICE 5 | 6 | recursive-exclude * __pycache__ 7 | recursive-exclude * *.py[co] 8 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Thomas Grainger and Scott Robertson 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | Portions of this Shipwright Software may utilize the following copyrighted material, the use of which is hereby acknowledged. 16 | 17 | Dockhand: https://github.com/graingert/dockhand 18 | 19 | Copyright 2016 Thomas Grainger 20 | 21 | Licensed under the Apache License, Version 2.0 (the "License"); 22 | you may not use this file except in compliance with the License. 23 | You may obtain a copy of the License at 24 | 25 | http://www.apache.org/licenses/LICENSE-2.0 26 | 27 | Unless required by applicable law or agreed to in writing, software 28 | distributed under the License is distributed on an "AS IS" BASIS, 29 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 30 | See the License for the specific language governing permissions and 31 | limitations under the License. 32 | 33 | Shipwright: https://github.com/6si/shipwright 34 | 35 | Copyright 2016 Scott Robertson 36 | 37 | Licensed under the Apache License, Version 2.0 (the "License"); 38 | you may not use this file except in compliance with the License. 39 | You may obtain a copy of the License at 40 | 41 | http://www.apache.org/licenses/LICENSE-2.0 42 | 43 | Unless required by applicable law or agreed to in writing, software 44 | distributed under the License is distributed on an "AS IS" BASIS, 45 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 46 | See the License for the specific language governing permissions and 47 | limitations under the License. 48 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Shipwright builds shared Docker images within a git repository in **the 2 | right order** and publishes them tagged with git's revision/branch 3 | information so you'll never lose track of an image's origin. 4 | 5 | It's the perfect tool for building and publishing your images to places 6 | like Docker Hub or your own private registry. Have a look at `our 7 | motivation `__ to see why we built it and the pain 8 | points it solves for you. 9 | 10 | Alternatives 11 | ============ 12 | 13 | See also https://github.com/moby/buildkit 14 | 15 | Installation 16 | ============ 17 | 18 | Shipwright is a simple python script you can install with pip 19 | 20 | :: 21 | 22 | $ pip install shipwright 23 | 24 | Quickstart 25 | ========== 26 | 27 | Once installed, simply change to a project of yours that contains 28 | multiple Dockerfiles and is in git. 29 | 30 | Add a json formatted file named ``.shipwright.json`` to the root 31 | directory of your project. At minimum it should contain the version 32 | number of your Shipwright config and a ``namespace`` which is either 33 | your docker hub user name or the URL to your private repository. 34 | 35 | 1.0 is the current version for the config. 36 | 37 | .. code:: json 38 | 39 | { 40 | "version": 1.0, 41 | "namespace": "[your docker hub name or private repository]" 42 | } 43 | 44 | Additionally your config file can map directory names to alternative 45 | docker repositories. For example here is a ``.shipwright.json`` for the 46 | docker hub user ``shipwright`` that also maps the root of the git 47 | repository to the docker image ``shipwright/shared`` and the ``/foo`` 48 | directory to ``shipwright/awesome_sauce``. 49 | 50 | .. code:: json 51 | 52 | { 53 | "version": 1.0, 54 | 55 | "namespace": "shipwright", 56 | "names": { 57 | "/": "shipwright/shared", 58 | "/foo": "shipwright/awesome_sauce" 59 | } 60 | 61 | Now you can build all the docker images in the git repo by simply 62 | changing to any directory under your git repo and running: 63 | 64 | :: 65 | 66 | $ shipwright 67 | 68 | This will recurse through all the directories, looking for ones that 69 | contain a Dockerfile. Shipwright will build these Dockerfiles in order 70 | and by default tag them with ``/:`` 71 | along with ``/:`` and 72 | ``/:latest`` 73 | 74 | Working example 75 | --------------- 76 | 77 | We have `a sample shipwright 78 | project `__ you can use if you 79 | want to try this out right away. 80 | 81 | .. code:: bash 82 | 83 | $ git clone https://github.com/6si/shipwright-sample.git 84 | $ cd shipwright-sample 85 | $ shipwright 86 | 87 | **NOTE: you can use any username you'd like while building locally. In 88 | the above example we use ``shipwright``. Nothing is published unless you 89 | use the ``push`` command. For your own projects, substitute 90 | ``shipwright`` in the above example with your (or your organizations) 91 | official docker hub username or private repository.** 92 | 93 | Notice that if you run the ``shipwright`` a second time it will return 94 | immediately without doing anything. Shipwright is smart enough to know 95 | nothing has changed. 96 | 97 | Shipwright really shines when you switch git branches. 98 | 99 | .. code:: bash 100 | 101 | $ git checkout new_feature 102 | $ shipwright 103 | 104 | Notice that shipwright only rebuilt the shared library and service1, 105 | ignoring the other projects because they have a common git ancestry. 106 | Running ``docker images`` however shows that all the images in the git 107 | repository have been tagged with the latest git revision, branch and 108 | ``latest``. 109 | 110 | In fact, as Shipwright builds images it rewrites the Dockerfiles so 111 | that they require the base images with tags from the current git 112 | revision. This ensures that the entire build is deterministic and 113 | reproducible. 114 | 115 | Building 116 | ======== 117 | 118 | By default, if you run shipwright with no arguments, it will build all 119 | Dockerfiles in your git repo. You can specify one or more ``specifiers`` 120 | to select fewer images to build. For example you can build a single 121 | images and its dependencies by simply specifying its name on the command 122 | line. 123 | 124 | :: 125 | 126 | $ shipwright /some_image 127 | 128 | Run \`shipwright --help' for more examples of specifiers and their uses. 129 | 130 | Publishing 131 | ========== 132 | 133 | With one command Shipwright can build your images and push them to a 134 | remote repository. 135 | 136 | :: 137 | 138 | $ shipwright push 139 | 140 | If you like you can just push your latest images without building. 141 | 142 | :: 143 | 144 | $ shipwright push --no-build 145 | 146 | The same specifiers for building also work with ``push``. You might use 147 | this to build an entire tree in one step then push a specific image like 148 | so. 149 | 150 | :: 151 | 152 | $ shipwright build 153 | $ shipwright push -e /public_image 154 | -------------------------------------------------------------------------------- /docs/.gitignore-gh-pages: -------------------------------------------------------------------------------- 1 | /_site 2 | -------------------------------------------------------------------------------- /docs/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:13.10 2 | RUN apt-get -qq update && apt-get install -y ruby1.8 bundler python 3 | RUN locale-gen en_US.UTF-8 4 | ADD Gemfile /code/ 5 | ADD Gemfile.lock /code/ 6 | WORKDIR /code 7 | RUN bundle install 8 | ADD . /code 9 | EXPOSE 4000 10 | CMD bundle exec jekyll build 11 | -------------------------------------------------------------------------------- /docs/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'github-pages' 4 | -------------------------------------------------------------------------------- /docs/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | RedCloth (4.2.9) 5 | blankslate (2.1.2.4) 6 | classifier (1.3.3) 7 | fast-stemmer (>= 1.0.0) 8 | colorator (0.1) 9 | commander (4.1.5) 10 | highline (~> 1.6.11) 11 | fast-stemmer (1.0.2) 12 | ffi (1.9.3) 13 | github-pages (12) 14 | RedCloth (= 4.2.9) 15 | jekyll (= 1.4.2) 16 | kramdown (= 1.2.0) 17 | liquid (= 2.5.4) 18 | maruku (= 0.7.0) 19 | rdiscount (= 2.1.7) 20 | redcarpet (= 2.3.0) 21 | highline (1.6.20) 22 | jekyll (1.4.2) 23 | classifier (~> 1.3) 24 | colorator (~> 0.1) 25 | commander (~> 4.1.3) 26 | liquid (~> 2.5.2) 27 | listen (~> 1.3) 28 | maruku (~> 0.7.0) 29 | pygments.rb (~> 0.5.0) 30 | redcarpet (~> 2.3.0) 31 | safe_yaml (~> 0.9.7) 32 | toml (~> 0.1.0) 33 | kramdown (1.2.0) 34 | liquid (2.5.4) 35 | listen (1.3.1) 36 | rb-fsevent (>= 0.9.3) 37 | rb-inotify (>= 0.9) 38 | rb-kqueue (>= 0.2) 39 | maruku (0.7.0) 40 | parslet (1.5.0) 41 | blankslate (~> 2.0) 42 | posix-spawn (0.3.8) 43 | pygments.rb (0.5.4) 44 | posix-spawn (~> 0.3.6) 45 | yajl-ruby (~> 1.1.0) 46 | rb-fsevent (0.9.4) 47 | rb-inotify (0.9.3) 48 | ffi (>= 0.5.0) 49 | rb-kqueue (0.2.0) 50 | ffi (>= 0.5.0) 51 | rdiscount (2.1.7) 52 | redcarpet (2.3.0) 53 | safe_yaml (0.9.7) 54 | toml (0.1.0) 55 | parslet (~> 1.5.0) 56 | yajl-ruby (1.1.0) 57 | 58 | PLATFORMS 59 | ruby 60 | 61 | DEPENDENCIES 62 | github-pages 63 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | markdown: redcarpet 2 | encoding: utf-8 3 | 4 | -------------------------------------------------------------------------------- /docs/imgs/image-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/6si/shipwright/f6fb93f82a83ddb856ab1a421c382302ac2e2e19/docs/imgs/image-graph.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Shipwright | The right way to build, tag and ship shared Docker images. 4 | --- 5 | 6 | Shipwright | The right way to build, tag and 7 | ship Docker images. 8 | 9 | 10 | Shipwright builds shared Docker images within a git repository 11 | in **the right order** and publishes them tagged with git's revision/branch 12 | information so you'll never loose track of an images origin. 13 | 14 | It's the perfect tool for building and publishing your images to places 15 | like Docker Hub or your own private registry. Have a look at [our motivation](motivation.md) to see why we built it and the pain points it solves for you. 16 | 17 | 18 | Installation 19 | ============ 20 | 21 | Shipwright is a simple python script you can install with pip 22 | 23 | $ pip install shipwright 24 | 25 | 26 | Quickstart 27 | ========== 28 | 29 | 30 | Building 31 | -------- 32 | 33 | Once installed, simply change to a project of yours that contains multiple Dockerfiles and is in git. Then run: 34 | 35 | $ shipwright 36 | 37 | This will recurse through all the directories and looking for ones that contain a Dockerfile. Shipwright will build these dockefiles in order and by default tag them with `/` 38 | 39 | 40 | We have [a sample shipwright project](https://github.com/6si/shipwrigt-sample) you can use if you want to try this out right away. 41 | 42 | ```bash 43 | $ git clone https://github.com/6si/shipwrigt-sample.git 44 | $ cd shipwright-sample 45 | $ shipwright solomon 46 | ``` 47 | 48 | **NOTE: you can use any username you'd like while building locally. Nothing is published unless you include the `--publish` flag. However it's probably a good idea to substitue `solomon` in the above example with you (or your organizations) official docker hub username.** 49 | 50 | **PRO TIP: if you build a lot, set the `SW_NAMESPACE` environment variable to your username** 51 | 52 | Running `shipwright` a second time nothing will return immediatly without doing anything as Shipwright is smart enough to know nothing has changed. 53 | 54 | Shipwright really shines when you switch git branches. 55 | 56 | ```bash 57 | $ git checkout new_feature 58 | $ shipwright solomon 59 | ``` 60 | 61 | Notice that shipwright only rebuilt the shared library and service1 ignoring the other projects because they have a common git ancestory. Running `docker images` however shows that all the images in the git repository have been tagged with the latest git revision and branch. 62 | 63 | In fact as Shipwright builds images it rewrites the Dockerfiles so that they require the base images with tags from the current git revision. This ensures that the entire build is deterministic and reproducable. 64 | 65 | Publishing 66 | ---------- 67 | TBD 68 | 69 | To publish the built images after building and tagging, simply include the `--publish` flag when running shipwright 70 | 71 | ```bash 72 | $ shipwright --publish 73 | ``` 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /docs/motivation.md: -------------------------------------------------------------------------------- 1 | #Motivation: 2 | 3 | 4 | Docker has a wonderful mechanism for distributing shared libraries or 5 | other common resources to images at build time. Simply create a 6 | [Dockerfile](https://docs.docker.com/reference/builder/) 7 | that builds your shared library first. Then create seperate Dockerfiles 8 | for each of your other services. Each of those images use your shared image 9 | as their base image in the [FROM](https://docs.docker.com/reference/builder/#from) 10 | clause. 11 | 12 | A typical distributed project at 6Sense looks something like this. 13 | 14 | ![6sense project](imgs/image-graph.png) 15 | 16 | And the directory structure would look something like 17 | 18 | $ find . 19 | ./shared/python/Dockerfile 20 | ./shared/python/setup.py 21 | ... 22 | ./shared/libs/Dockerfile 23 | ./shared/libs/src 24 | ... 25 | ./service1/Dockerfile 26 | ... 27 | ./service2/Dockerfile 28 | ... 29 | ./admin/Dockerfile 30 | ... 31 | 32 | 33 | 34 | However, Docker leaves it up to you to build these images in the right order. If you checked out this code directly from git and tried to build 35 | `service1` it would fail. It depends on `6sense/shared` which hasn't been built yet. In fact a typical session by hand would looks something like this. 36 | 37 | 38 | $ cd 39 | $ docker build -t 6sense/python ./shared/python 40 | $ docker build -t 6sense/shared ./shared/libs 41 | $ docker build -t 6sense/admin ./admin 42 | $ docker build -t 6sense/service1 ./service1 43 | $ docker build -t 6sense/service1 ./service2 44 | 45 | Now that's annoying, but manageable. As experinced Unix developers, we started off with the naive approach of using a `Makefile` to build all the images in the right order. 46 | 47 | But we kept running into other problems. We like to develop using feature branches. Often during the same day we'd switch back and forth between several git branches, make changes to the various images, rebuild, test think everything was hunky dory. Until we'd run into strange errors. Only after several frustrating hours of debuging would we realize we pushed images from mixed branches or built images in the wrong order. 48 | 49 | The solution to that problem was to include the git branch name in all the images we built as a tag, but also meant we had to update the Dockerfile and ensure the `FROM` included the right tag as in `FROM 6sense/shared:`. But this too had problems, if you had two developers working on the same branch. 50 | 51 | Our build scripts kept growing ever more elaborate. Then it dawned on us that most of the information needed to build and tag images properly is already present in some form either in the Dockerfile or within git itself. 52 | 53 | What we really wanted was a simple tool that would read all the Dockerfiles in a git checkout, determine the proper build order, then build and tag them. And if a base image changed we wanted to make sure all the dependent images got rebuilt in turn. Being that building could be slow (even with Docker's awsesome build caching), if there were no changes to certain images in the build group, simply tag them with the latest git revision. 54 | 55 | It didn't exist, so we built [Shipwright](index.md) 56 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.rst 3 | 4 | [bdist_wheel] 5 | universal=1 6 | 7 | [zest.releaser] 8 | create-wheel = yes 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, print_function 2 | 3 | import io 4 | import os 5 | 6 | from setuptools import find_packages, setup 7 | 8 | 9 | def read(*names, **kwargs): 10 | with io.open( 11 | os.path.join(os.path.dirname(__file__), *names), 12 | encoding=kwargs.get('encoding', 'utf8'), 13 | ) as fp: 14 | return fp.read() 15 | 16 | 17 | readme = open('README.rst').read() 18 | history = open('CHANGES.rst').read().replace('.. :changelog:', '') 19 | 20 | 21 | setup( 22 | name='shipwright', 23 | version='0.9.1.dev0', 24 | url='https://github.com/6si/shipwright/', 25 | license='Apache Software License', 26 | author='Scott Robertson', 27 | tests_require=['nose'], 28 | install_requires=[ 29 | 'docker>=2.0.1, <3.0.0', 30 | 'GitPython>=2.0.5, <3.0.0', 31 | ], 32 | extras_require={ 33 | 'registry': ['docker-registry-client>=0.5.1, <0.6.0'], 34 | }, 35 | author_email='scott@6sense.com', 36 | description=( 37 | 'The right way to build, tag and ship shared Docker images.' 38 | ), 39 | long_description=readme + '\n\n' + history, 40 | packages=find_packages(), 41 | include_package_data=True, 42 | platforms='any', 43 | classifiers=[ 44 | 'Programming Language :: Python', 45 | 'Programming Language :: Python :: 2', 46 | 'Programming Language :: Python :: 2.7', 47 | 'Programming Language :: Python :: 3', 48 | 'Programming Language :: Python :: 3.4', 49 | 'Programming Language :: Python :: 3.5', 50 | 'Programming Language :: Python :: 3.6', 51 | 'Development Status :: 4 - Beta', 52 | 'Natural Language :: English', 53 | 'Intended Audience :: Developers', 54 | 'License :: OSI Approved :: Apache Software License', 55 | 'Operating System :: OS Independent', 56 | 'Topic :: Software Development :: Libraries :: Python Modules', 57 | ], 58 | entry_points={ 59 | 'console_scripts': [ 60 | 'shipwright = shipwright._lib.cli:main', 61 | ], 62 | }, 63 | ) 64 | -------------------------------------------------------------------------------- /shipwright/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/6si/shipwright/f6fb93f82a83ddb856ab1a421c382302ac2e2e19/shipwright/__init__.py -------------------------------------------------------------------------------- /shipwright/__main__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from shipwright._lib.cli import main as _main 4 | 5 | if __name__ == '__main__': 6 | _main() 7 | -------------------------------------------------------------------------------- /shipwright/_lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/6si/shipwright/f6fb93f82a83ddb856ab1a421c382302ac2e2e19/shipwright/_lib/__init__.py -------------------------------------------------------------------------------- /shipwright/_lib/base.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from . import build, dependencies 4 | from .msg import BuildComplete 5 | 6 | 7 | class Shipwright(object): 8 | def __init__(self, source_control, docker_client, tags, cache): 9 | self.source_control = source_control 10 | self.docker_client = docker_client 11 | self.tags = tags 12 | self._cache = cache 13 | 14 | def targets(self): 15 | return self.source_control.targets() 16 | 17 | def build(self, build_targets): 18 | targets = dependencies.eval(build_targets, self.targets()) 19 | this_ref_str = self.source_control.this_ref_str() 20 | return self._build(this_ref_str, targets) 21 | 22 | def _build(self, this_ref_str, targets): 23 | client = self.docker_client 24 | ref = this_ref_str 25 | tags = self.source_control.default_tags() + self.tags + [this_ref_str] 26 | for evt in build.do_build(client, ref, targets, self._cache): 27 | if isinstance(evt, BuildComplete): 28 | target = evt.target 29 | for tag_evt in self._cache.tag([target], tags): 30 | yield tag_evt 31 | yield evt 32 | 33 | def images(self, build_targets): 34 | for target in dependencies.eval(build_targets, self.targets()): 35 | yield { 36 | 'stream': '{t.name}:{t.ref}'.format(t=target), 37 | 'event': 'log', 38 | } 39 | 40 | def push(self, build_targets, no_build=False): 41 | """ 42 | Pushes the latest images to the repository. 43 | """ 44 | targets = dependencies.eval(build_targets, self.targets()) 45 | this_ref_str = self.source_control.this_ref_str() 46 | tags = self.source_control.default_tags() + self.tags + [this_ref_str] 47 | 48 | if no_build: 49 | for evt in self._cache.push(targets, tags): 50 | yield evt 51 | return 52 | 53 | for evt in self._build(this_ref_str, targets): 54 | if isinstance(evt, BuildComplete): 55 | for push_evt in self._cache.push([evt.target], tags): 56 | yield push_evt 57 | yield evt 58 | -------------------------------------------------------------------------------- /shipwright/_lib/build.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from . import docker 4 | from .msg import BuildComplete 5 | 6 | 7 | def _merge(d1, d2): 8 | d = d1.copy() 9 | d.update(d2) 10 | return d 11 | 12 | 13 | def do_build(client, build_ref, targets, cache): 14 | """ 15 | Generic function for building multiple images while 16 | notifying a callback function with output produced. 17 | 18 | Given a list of targets it builds the target with the given 19 | build_func while streaming the output through the given 20 | show_func. 21 | 22 | Returns an iterator of (image, docker_image_id) pairs as 23 | the final output. 24 | 25 | Building an image can take sometime so the results are returned as 26 | an iterator in case the caller wants to use restults in between builds. 27 | 28 | The consequences of this is you must either call it as part of a for loop 29 | or pass it to a function like list() which can consume an iterator. 30 | 31 | """ 32 | 33 | build_index = {t.image.name: t.ref for t in targets} 34 | 35 | for target in targets: 36 | parent_ref = None 37 | if target.parent: 38 | parent_ref = build_index.get(target.parent) 39 | for evt in build(client, parent_ref, target, cache): 40 | yield evt 41 | yield BuildComplete(target) 42 | 43 | 44 | def build(client, parent_ref, image, cache): 45 | """ 46 | builds the given image tagged with and ensures that 47 | it depends on it's parent if it's part of this build group (shares 48 | the same namespace) 49 | """ 50 | 51 | merge_config = { 52 | 'event': 'build_msg', 53 | 'target': image, 54 | 'rev': image.ref, 55 | } 56 | 57 | def process_event_(evt): 58 | return _merge(merge_config, evt) 59 | 60 | built_tags = docker.last_built_from_docker(client, image.name) 61 | if image.ref in built_tags: 62 | return 63 | 64 | for evt in cache.build(parent_ref, image): 65 | yield process_event_(evt) 66 | -------------------------------------------------------------------------------- /shipwright/_lib/cache.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import os 4 | import sys 5 | import traceback 6 | 7 | from docker import errors as d_errors 8 | from requests import exceptions as requests_exceptions 9 | 10 | from . import compat, docker, push, tar 11 | 12 | 13 | def pull(client, *args, **kwargs): 14 | try: 15 | for evt in client.pull(*args, **kwargs): 16 | yield compat.json_loads(evt) 17 | except d_errors.NotFound as e: 18 | yield docker.error(e.explanation) 19 | 20 | 21 | class PullFailedException(Exception): 22 | pass 23 | 24 | 25 | class CacheMissException(Exception): 26 | pass 27 | 28 | 29 | _FAILED = object() 30 | 31 | 32 | class NoCache(object): 33 | def __init__(self, docker_client): 34 | self.docker_client = docker_client 35 | self._pulled_images = {} 36 | 37 | def _pull_cache(self, image): 38 | raise CacheMissException() 39 | yield 40 | 41 | def tag(self, targets, tags): 42 | for image in targets: 43 | for tag in tags: 44 | yield docker.tag_image( 45 | self.docker_client, 46 | image, 47 | tag, 48 | ) 49 | 50 | def push(self, targets, tags): 51 | names_and_tags = set() 52 | for image in targets: 53 | names_and_tags.add((image.name, image.ref)) 54 | for tag in tags: 55 | names_and_tags.add((image.name, tag)) 56 | 57 | for evt in push.do_push(self.docker_client, sorted(names_and_tags)): 58 | yield evt 59 | 60 | names_and_tags = set() 61 | names_and_tags.add((image.name, image.ref)) 62 | for tag in tags: 63 | names_and_tags.add((image.name, tag)) 64 | 65 | for evt in push.do_push(self.docker_client, sorted(names_and_tags)): 66 | yield evt 67 | 68 | def build(self, parent_ref, image): 69 | repo = image.name 70 | tag = image.ref 71 | client = self.docker_client 72 | 73 | try: 74 | for evt in self._pull_cache(image): 75 | yield evt 76 | except CacheMissException: 77 | pass 78 | else: 79 | return 80 | 81 | # pull the parent if it has not been built because Docker-py fails 82 | # to send the correct credentials in the build command. 83 | if parent_ref: 84 | try: 85 | for evt in self._pull(image.parent, parent_ref): 86 | yield evt 87 | except PullFailedException: 88 | pass 89 | 90 | build_evts = client.build( 91 | fileobj=tar.mkcontext(parent_ref, image.path), 92 | rm=True, 93 | custom_context=True, 94 | stream=True, 95 | tag='{0}:{1}'.format(image.name, image.ref), 96 | dockerfile=os.path.basename(image.path), 97 | ) 98 | 99 | for evt in build_evts: 100 | yield compat.json_loads(evt) 101 | 102 | self._pulled_images[(repo, tag)] = True 103 | 104 | def _pull(self, repo, tag): 105 | already_pulled = self._pulled_images.get((repo, tag), False) 106 | if already_pulled is _FAILED: 107 | raise PullFailedException() 108 | 109 | if already_pulled: 110 | return 111 | 112 | client = self.docker_client 113 | 114 | failed = False 115 | pull_evts = pull( 116 | client, 117 | repository=repo, 118 | tag=tag, 119 | stream=True, 120 | ) 121 | for event in pull_evts: 122 | if 'error' in event: 123 | event['warn'] = event['error'] 124 | del event['error'] 125 | failed = True 126 | yield event 127 | 128 | if failed: 129 | self._pulled_images[(repo, tag)] = _FAILED 130 | raise PullFailedException() 131 | 132 | self._pulled_images[(repo, tag)] = True 133 | 134 | 135 | class Cache(NoCache): 136 | def _pull_cache(self, image): 137 | pull_events = self._pull(repo=image.name, tag=image.ref) 138 | try: 139 | for evt in pull_events: 140 | yield evt 141 | except PullFailedException: 142 | raise CacheMissException() 143 | 144 | 145 | class DirectRegistry(NoCache): 146 | def __init__(self, docker_client, docker_registry): 147 | super(DirectRegistry, self).__init__(docker_client) 148 | self.drc = docker_registry 149 | self._cache = {} 150 | 151 | def _get_manifest(self, tag): 152 | name, ref = tag 153 | try: 154 | return self._cache[tag] 155 | except KeyError: 156 | try: 157 | m = self.drc.get_manifest(name, ref) 158 | except requests_exceptions.HTTPError: 159 | return None 160 | else: 161 | self._cache[tag] = m 162 | return m 163 | 164 | def _put_manifest(self, tag, manifest): 165 | name, ref = tag 166 | if manifest is None: 167 | msg = 'manifest does not exist, did the image fail to build?' 168 | yield docker.error(msg) 169 | return 170 | try: 171 | self.drc.put_manifest(name, ref, manifest) 172 | except requests_exceptions.HTTPError: 173 | msg = traceback.format_exception(*sys.exc_info()) 174 | yield docker.error(msg) 175 | else: 176 | yield {} 177 | 178 | def _pull_cache(self, image): 179 | tag = (image.name, image.ref) 180 | if self._get_manifest(tag) is None: 181 | raise CacheMissException() 182 | return 183 | yield 184 | 185 | def tag(self, targets, tags): 186 | """ 187 | A noop operation because we can't tag locally, if we don't have the 188 | built images 189 | """ 190 | return 191 | yield 192 | 193 | def push(self, targets, tags): 194 | to_push = set() 195 | to_alias = [] 196 | for image in targets: 197 | tag = (image.name, image.ref) 198 | manifest = self._get_manifest(tag) 199 | if manifest is not None: 200 | to_alias.append((tag, manifest)) 201 | else: 202 | to_push.add(tag) 203 | 204 | sorted_to_push = sorted(to_push) 205 | for evt in push.do_push(self.docker_client, sorted_to_push): 206 | yield evt 207 | 208 | for tag in sorted_to_push: 209 | manifest = self._get_manifest(tag) 210 | to_alias.append((tag, manifest)) 211 | 212 | for (name, ref), manifest in to_alias: 213 | for tag in tags: 214 | dest = (name, tag) 215 | extra = { 216 | 'event': 'alias', 217 | 'old_image': name + ':' + ref, 218 | 'repository': name, 219 | 'tag': tag, 220 | } 221 | for evt in self._put_manifest(dest, manifest): 222 | evt.update(extra) 223 | yield evt 224 | -------------------------------------------------------------------------------- /shipwright/_lib/cli.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Environment Variables: 4 | 5 | SW_NAMESPACE : If DOCKER_HUB_ACCOUNT is not passed on the command line 6 | this Environment variable must be present. 7 | 8 | DOCKER_HOST : Same URL as used by the docker client to connect to 9 | the docker daemon. 10 | 11 | Examples: 12 | 13 | Assuming adependencies tree that looks like this. 14 | 15 | ubuntu 16 | └─── base 17 | └─── shared 18 | | ├─── service1 19 | | | └─── service2 20 | | └─── service3 21 | └─── independent 22 | 23 | 24 | Build everything: 25 | 26 | $ shipwright build 27 | 28 | Build base, shared and service1: 29 | 30 | $ shipwright build -u service1 31 | 32 | Build base, shared and service1, service2: 33 | 34 | $ shipwright build -d service1 35 | 36 | Use exclude to build base, shared and service1, service2: 37 | 38 | $ shipwright build -x service3 -x independent 39 | 40 | Build base, independent, shared and service3 41 | 42 | $ shipwright build -x service1 43 | 44 | Build base, independent, shared and service1, service2: 45 | 46 | $ shipwright build -d service1 -u independent 47 | 48 | Note that specfying a TARGET is the same as -u so the following 49 | command is equivalent to the one above. 50 | 51 | $ shipwright build -d service1 independent 52 | 53 | 54 | """ 55 | from __future__ import absolute_import, print_function 56 | 57 | import argparse 58 | import collections 59 | import json 60 | import os 61 | import re 62 | import shlex 63 | import sys 64 | from itertools import chain, cycle 65 | 66 | import docker 67 | from docker.utils import kwargs_from_env 68 | 69 | from . import cache, registry, source_control 70 | from .base import Shipwright 71 | from .colors import rainbow 72 | from .msg import Message 73 | 74 | try: 75 | import docker_registry_client as drc 76 | except ImportError as e: 77 | drc = e 78 | 79 | 80 | def argparser(): 81 | def a_arg(parser, *args, **kwargs): 82 | default = kwargs.pop('default', []) 83 | parser.add_argument( 84 | *args, 85 | action='append', 86 | nargs='*', 87 | default=default, 88 | **kwargs 89 | ) 90 | 91 | desc = 'Builds shared Docker images within a common repository' 92 | parser = argparse.ArgumentParser(description=desc) 93 | parser.add_argument( 94 | '-H', '--docker-host', 95 | help="Override DOCKER_HOST if it's set in the environment.", 96 | ) 97 | parser.add_argument( 98 | '--dump-file', 99 | help='Save raw events to json to FILE, Useful for debugging', 100 | type=argparse.FileType('w'), 101 | ) 102 | parser.add_argument( 103 | '--x-assert-hostname', 104 | action='store_true', 105 | help='Disable strict hostchecking, useful for boot2docker.', 106 | ) 107 | parser.add_argument( 108 | '--account', 109 | help="Override SW_NAMESPACE if it's set in the environment.", 110 | ) 111 | 112 | subparsers = parser.add_subparsers(help='sub-command help', dest='command') 113 | subparsers.required = True 114 | 115 | common = argparse.ArgumentParser(add_help=False) 116 | common.add_argument( 117 | '--dirty', 118 | help='Build working tree, including uncommited and untracked changes', 119 | action='store_true', 120 | ) 121 | common.add_argument( 122 | '--pull-cache', 123 | help='When building try to pull previously built images', 124 | action='store_true', 125 | ) 126 | a_arg( 127 | common, '--registry-login', 128 | help=( 129 | 'When pulling cache and pushing tags, talk to a registry directly ' 130 | 'where possible' 131 | ), 132 | ) 133 | a_arg( 134 | common, '-d', '--dependants', 135 | help='Build DEPENDANTS and all its dependants', 136 | ) 137 | a_arg( 138 | common, '-e', '--exact', 139 | help='Build EXACT only - may fail if dependencies have not been built', 140 | ) 141 | a_arg( 142 | common, '-u', '--upto', 143 | help='Build UPTO and it dependencies', 144 | ) 145 | a_arg( 146 | common, '-x', '--exclude', 147 | help=' Build everything but EXCLUDE and its dependents', 148 | ) 149 | a_arg( 150 | common, '-t', '--tag', 151 | dest='tags', 152 | help='extra tags to apply to the images', 153 | ) 154 | 155 | subparsers.add_parser( 156 | 'build', help='builds images', parents=[common], 157 | ) 158 | 159 | subparsers.add_parser( 160 | 'images', help='lists images to build', parents=[common], 161 | ) 162 | 163 | push = subparsers.add_parser( 164 | 'push', help='pushes built images', parents=[common], 165 | ) 166 | push.add_argument('--no-build', action='store_true') 167 | 168 | return parser 169 | 170 | 171 | def parse_registry_logins(registry_logins): 172 | parser = argparse.ArgumentParser(description='--registry-login') 173 | parser.add_argument('-u', '--username', nargs='?', help='Username') 174 | parser.add_argument('-p', '--password', nargs='?', help='Password') 175 | parser.add_argument('-e', '--email', nargs='?', help='Email') 176 | parser.add_argument('server', help='SERVER') 177 | 178 | registries = {} 179 | for login in registry_logins: 180 | args = shlex.split(re.sub(r'^docker login ', '', login, count=1)) 181 | ns, _ = parser.parse_known_args(args) 182 | server = re.sub(r'^https?://', '', ns.server, count=1) 183 | registries[server] = { 184 | 'username': ns.username, 185 | 'password': ns.password, 186 | 'server': ns.server, 187 | } 188 | 189 | return registries 190 | 191 | 192 | def _flatten(items): 193 | return list(chain.from_iterable(items)) 194 | 195 | 196 | def old_style_arg_dict(namespace): 197 | ns = namespace 198 | return { 199 | '--account': ns.account, 200 | '--dependents': _flatten(ns.dependants), 201 | '--dump-file': ns.dump_file, 202 | '--exact': _flatten(ns.exact), 203 | '--exclude': _flatten(ns.exclude), 204 | '--help': False, 205 | '--no-build': getattr(ns, 'no_build', False), 206 | '--upto': _flatten(ns.upto), 207 | '--x-assert-hostname': ns.x_assert_hostname, 208 | '-H': ns.docker_host, 209 | 'TARGET': [], 210 | 'build': ns.command == 'build', 211 | 'push': ns.command == 'push', 212 | 'images': ns.command == 'images', 213 | 'tags': sorted(set(_flatten(ns.tags))) or ['latest'], 214 | } 215 | 216 | 217 | def main(): 218 | arguments = argparser().parse_args() 219 | old_style_args = old_style_arg_dict(arguments) 220 | return run( 221 | path=os.getcwd(), 222 | arguments=old_style_args, 223 | client_cfg=kwargs_from_env(), 224 | environ=os.environ, 225 | new_style_args=arguments, 226 | ) 227 | 228 | 229 | def process_arguments(path, arguments, client_cfg, environ): 230 | try: 231 | config = json.load(open( 232 | os.path.join(path, '.shipwright.json'), 233 | )) 234 | except IOError: 235 | config = { 236 | 'namespace': ( 237 | arguments['--account'] or 238 | environ.get('SW_NAMESPACE') 239 | ), 240 | } 241 | if config['namespace'] is None: 242 | exit( 243 | 'Please specify your docker hub account in\n' 244 | 'the .shipwright.json config file,\n ' 245 | 'the command line or set SW_NAMESPACE.\n' 246 | 'Run shipwright --help for more information.', 247 | ) 248 | assert_hostname = config.get('assert_hostname') 249 | if arguments['--x-assert-hostname']: 250 | assert_hostname = not arguments['--x-assert-hostname'] 251 | 252 | tls_config = client_cfg.get('tls') 253 | if tls_config is not None: 254 | tls_config.assert_hostname = assert_hostname 255 | 256 | client = docker.APIClient(version='1.18', **client_cfg) 257 | commands = ['build', 'push', 'images'] 258 | command_names = [c for c in commands if arguments.get(c)] 259 | command_name = command_names[0] if command_names else 'build' 260 | 261 | build_targets = { 262 | 'exact': arguments['--exact'], 263 | 'dependents': arguments['--dependents'], 264 | 'exclude': arguments['--exclude'], 265 | 'upto': arguments['--upto'], 266 | } 267 | 268 | no_build = False 269 | if command_name == 'push': 270 | no_build = arguments['--no-build'] 271 | dump_file = None 272 | if arguments['--dump-file']: 273 | dump_file = open(arguments['--dump-file'], 'w') 274 | 275 | return build_targets, no_build, command_name, dump_file, config, client 276 | 277 | 278 | class SetJSONEncoder(json.JSONEncoder): 279 | def default(self, obj): 280 | if isinstance(obj, collections.Set): 281 | return sorted(obj) 282 | return super(SetJSONEncoder, self).decode(obj) 283 | 284 | 285 | def run(path, arguments, client_cfg, environ, new_style_args=None): 286 | args = process_arguments( 287 | path, arguments, client_cfg, environ, 288 | ) 289 | build_targets, no_build, command_name, dump_file, config, client = args 290 | 291 | if new_style_args is None: 292 | dirty = False 293 | pull_cache = False 294 | registry_logins = [] 295 | else: 296 | dirty = new_style_args.dirty 297 | pull_cache = new_style_args.pull_cache 298 | registry_logins = _flatten(new_style_args.registry_login) 299 | 300 | namespace = config['namespace'] 301 | name_map = config.get('names', {}) 302 | scm = source_control.source_control(path, namespace, name_map) 303 | if not dirty and scm.is_dirty(): 304 | return ( 305 | 'Aborting build, due to uncommitted changes. If you are not ready ' 306 | 'to commit these changes, re-run with the --dirty flag.' 307 | ) 308 | 309 | if registry_logins: 310 | if isinstance(drc, Exception): 311 | raise drc 312 | 313 | registry_config = parse_registry_logins(registry_logins) 314 | registries = {} 315 | for server, config in registry_config.items(): 316 | registries[server] = drc.BaseClient( 317 | config['server'], 318 | username=config['username'], 319 | password=config['password'], 320 | api_version=2, 321 | ) 322 | the_cache = cache.DirectRegistry(client, registry.Registry(registries)) 323 | elif pull_cache: 324 | the_cache = cache.Cache(client) 325 | else: 326 | the_cache = cache.NoCache(client) 327 | 328 | sw = Shipwright(scm, client, arguments['tags'], the_cache) 329 | command = getattr(sw, command_name) 330 | 331 | show_progress = sys.stdout.isatty() 332 | 333 | errors = [] 334 | 335 | if no_build: 336 | events = command(build_targets, no_build) 337 | else: 338 | events = command(build_targets) 339 | 340 | for event in events: 341 | if isinstance(event, Message): 342 | continue 343 | if dump_file: 344 | json.dump(event, dump_file, cls=SetJSONEncoder) 345 | dump_file.write('\n') 346 | if 'error' in event: 347 | errors.append(event) 348 | msg = pretty_event(event, show_progress) 349 | if msg is not None: 350 | print(msg) 351 | 352 | if errors: 353 | print('The following errors occurred:', file=sys.stdout) 354 | messages = [pretty_event(error, True) for error in errors] 355 | for msg in sorted(m for m in messages if m is not None): 356 | print(msg, file=sys.stdout) 357 | sys.exit(1) 358 | 359 | 360 | def exit(msg): 361 | print(msg) 362 | sys.exit(1) 363 | 364 | 365 | def memo(f, arg, memos={}): 366 | if arg in memos: 367 | return memos[arg] 368 | else: 369 | memos[arg] = f(arg) 370 | return memos[arg] 371 | 372 | 373 | def pretty_event(evt, show_progress): 374 | formatted_message = switch(evt, show_progress) 375 | if formatted_message is None: 376 | return 377 | if not (evt['event'] in ('build_msg', 'push') or 'error' in evt): 378 | return formatted_message 379 | 380 | name = None 381 | target = evt.get('target') 382 | if target is not None: 383 | name = target.name 384 | else: 385 | name = evt.get('image') 386 | prettify = memo( 387 | highlight, 388 | name, 389 | ) 390 | return prettify(formatted_message) 391 | 392 | 393 | colors = cycle(rainbow()) 394 | 395 | 396 | def highlight(name): 397 | color_fn = next(colors) 398 | 399 | def highlight_(msg): 400 | return color_fn(name) + ' | ' + msg 401 | return highlight_ 402 | 403 | 404 | def switch(rec, show_progress): 405 | 406 | if 'stream' in rec: 407 | return rec['stream'].strip('\n') 408 | 409 | elif 'status' in rec: 410 | status = '[STATUS] {0}: {1}'.format( 411 | rec.get('id', ''), 412 | rec['status'], 413 | ) 414 | 415 | progress = rec.get('progressDetail') 416 | if progress: 417 | if show_progress: 418 | return '{status} {p[current]}/{p[total]}\r'.format( 419 | status=status, 420 | p=progress, 421 | ) 422 | return None 423 | return status 424 | 425 | elif 'error' in rec: 426 | return '[ERROR] {0}'.format(rec['errorDetail']['message']) 427 | elif 'warn' in rec: 428 | return '[WARN] {0}'.format(rec['errorDetail']['message']) 429 | elif rec['event'] == 'tag': 430 | fmt = 'Tagging {rec[old_image]} to {rec[repository]}:{rec[tag]}' 431 | return fmt.format(rec=rec) 432 | elif rec['event'] == 'alias': 433 | fmt = 'Fast-aliased {rec[old_image]} to {rec[repository]}:{rec[tag]}' 434 | return fmt.format(rec=rec) 435 | elif rec['event'] == 'push' and 'aux' in rec: 436 | return None 437 | else: 438 | return json.dumps(rec) 439 | -------------------------------------------------------------------------------- /shipwright/_lib/colors.py: -------------------------------------------------------------------------------- 1 | # borrowed from fig 2 | from __future__ import absolute_import, unicode_literals 3 | 4 | NAMES = [ 5 | 'grey', 6 | 'red', 7 | 'green', 8 | 'yellow', 9 | 'blue', 10 | 'magenta', 11 | 'cyan', 12 | 'white', 13 | ] 14 | 15 | 16 | def get_pairs(): 17 | for i, name in enumerate(NAMES): 18 | yield(name, str(30 + i)) 19 | yield('intense_' + name, str(30 + i) + ';1') 20 | 21 | 22 | def ansi(code): 23 | return '\033[{0}m'.format(code) 24 | 25 | 26 | def ansi_color(code, s): 27 | return '{0}{1}{2}'.format(ansi(code), s, ansi(0)) 28 | 29 | 30 | def make_color_fn(code): 31 | return lambda s: ansi_color(code, s) 32 | 33 | 34 | for (name, code) in get_pairs(): 35 | globals()[name] = make_color_fn(code) 36 | 37 | 38 | def rainbow(): 39 | cs = ['cyan', 'yellow', 'green', 'magenta', 'red', 'blue', 40 | 'intense_cyan', 'intense_yellow', 'intense_green', 41 | 'intense_magenta', 'intense_red', 'intense_blue'] 42 | 43 | for c in cs: 44 | yield globals()[c] 45 | -------------------------------------------------------------------------------- /shipwright/_lib/compat.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import json 4 | import sys 5 | 6 | PY2 = sys.version_info[0] == 2 7 | 8 | 9 | if PY2: 10 | json_loads = json.loads 11 | else: 12 | def json_loads(s, **kwargs): 13 | if isinstance(s, bytes): 14 | s = s.decode('utf8') 15 | return json.loads(s, **kwargs) 16 | -------------------------------------------------------------------------------- /shipwright/_lib/dependencies.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import operator 4 | from collections import namedtuple 5 | 6 | from . import zipper 7 | 8 | 9 | # [(tree -> [ImageNames])] -> [Images] 10 | def eval(build_targets, targets): 11 | """ 12 | Given a list of partially applied functions that 13 | take a tree and return a list of image names. 14 | 15 | First apply all non-exclude functinons with the tree built from targets 16 | creating a union of the results. 17 | 18 | Then returns the results of applying each exclusion functinon 19 | in order. 20 | 21 | """ 22 | tree = _make_tree(targets) 23 | bt = build_targets 24 | 25 | exact, dependents, upto = bt['exact'], bt['dependents'], bt['upto'] 26 | if exact or dependents or upto: 27 | base = set() 28 | for target in exact: 29 | base = base | set(_exact(target, tree)) 30 | for target in dependents: 31 | base = base | set(_dependents(target, tree)) 32 | for target in upto: 33 | base = base | set(_upto(target, tree)) 34 | 35 | tree = _make_tree(base) 36 | 37 | for target in bt['exclude']: 38 | tree = _exclude(target, tree) 39 | 40 | return _brood(tree) 41 | 42 | 43 | _Root = namedtuple('_Root', ['name', 'short_name', 'children']) 44 | 45 | 46 | def _find(tree, name): 47 | def find_(loc): 48 | target = loc.node() 49 | return target.name == name or target.short_name == name 50 | 51 | return tree.find(find_) 52 | 53 | 54 | def _make_tree(images): 55 | """ 56 | Converts a list of images into a tree represented by a zipper. 57 | see http://en.wikipedia.org/wiki/Zipper_(data_structure) 58 | """ 59 | 60 | root = _Root(None, None, ()) 61 | tree = zipper.zipper(root, _is_branch, _children, _make_node) 62 | 63 | for c in images: 64 | 65 | def is_child(target): 66 | if not isinstance(target, _Root): 67 | return target.parent == c.name 68 | 69 | branch_children, root_children = _split(is_child, tree.children()) 70 | t = c._replace(children=tuple(branch_children)) 71 | 72 | if branch_children: 73 | tree = tree.edit(_replace, tuple(root_children)) 74 | 75 | loc = _find(tree, t.parent) 76 | if loc: 77 | tree = loc.insert(t).top() 78 | else: 79 | tree = tree.insert(t) 80 | 81 | return tree 82 | 83 | 84 | def _replace(node, children): 85 | return node._replace(children=children) 86 | 87 | 88 | def _children(item): 89 | return item.children 90 | 91 | 92 | def _is_branch(item): 93 | return True 94 | 95 | 96 | def _make_node(node, children): 97 | # keep children sorted to make testing easier 98 | ch = tuple(sorted(children, key=operator.attrgetter('name'))) 99 | return node._replace(children=ch) 100 | 101 | 102 | def _breadth_first_iter(loc): 103 | """ 104 | Given a loctation node (from a zipper) walk it's children in breadth first 105 | order. 106 | """ 107 | 108 | tocheck = [loc] 109 | while tocheck: 110 | l = tocheck.pop(0) 111 | yield l 112 | child = l.down() 113 | while child: 114 | tocheck.append(child) 115 | child = child.right() 116 | 117 | 118 | # Loc -> [Target] 119 | def _lineage(loc): 120 | results = [] 121 | while loc.path: 122 | node = loc.node() 123 | results.append(node) 124 | loc = loc.up() 125 | return results 126 | 127 | 128 | # (a -> Bool) -> [a] ->[a], [a] 129 | def _split(f, children): 130 | """ 131 | Given a function that returns true or false and a list. Return 132 | a two lists all items f(child) == True is in list 1 and 133 | all items not in the list are in list 2. 134 | """ 135 | 136 | l1 = [] 137 | l2 = [] 138 | for child in children: 139 | if f(child): 140 | l1.append(child) 141 | else: 142 | l2.append(child) 143 | return l1, l2 144 | 145 | 146 | # Loc -> [Target] 147 | def _brood(loc): 148 | return [loc.node() for loc in _breadth_first_iter(loc)][1:] 149 | 150 | 151 | # Target -> Tree -> [Target] 152 | def _upto(target, tree): 153 | """Returns target and everything it depends on""" 154 | 155 | loc = _find(tree, target) 156 | 157 | return _lineage(loc) # _make_tree(lineage(loc)) 158 | 159 | 160 | # Target -> Tree -> [Target] 161 | def _dependents(target, tree): 162 | """Returns a target it's dependencies and everything that depends on it""" 163 | 164 | loc = _find(tree, target) 165 | 166 | return _lineage(loc) + _brood(loc) 167 | 168 | 169 | # Target -> Tree -> [Target] 170 | def _exact(target, tree): 171 | """Returns only the target.""" 172 | 173 | loc = _find(tree, target) 174 | 175 | return [loc.node()] 176 | 177 | 178 | # Target -> Tree -> Tree 179 | def _exclude(target, tree): 180 | """ 181 | Returns everything but the target and it's dependents. If target 182 | is not found the whole tree is returned. 183 | """ 184 | 185 | loc = _find(tree, target) 186 | if loc: 187 | return loc.remove().top() 188 | else: 189 | return tree 190 | -------------------------------------------------------------------------------- /shipwright/_lib/docker.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from docker import errors as d_errors 4 | 5 | 6 | def key_from_image_name(image_name): 7 | """ 8 | >>> key_from_image_name('shipwright/blah:1234') 9 | '1234' 10 | """ 11 | return image_name.split(':', 1)[1] 12 | 13 | 14 | def key_from_image_info(image_info_dict): 15 | """ 16 | >>> key_from_image_info({ 17 | ... 'RepoTags': [ 18 | ... 'shipwright/base:6e29823388f8', 'shipwright/base:test', 19 | ... ] 20 | ... }) 21 | ['6e29823388f8', 'test'] 22 | """ 23 | return [key_from_image_name(t) for t in image_info_dict['RepoTags']] 24 | 25 | 26 | def last_built_from_docker(client, name): 27 | images = client.images(name) 28 | built_tags = set() 29 | for image in images: 30 | if image['RepoTags'] is None: 31 | continue 32 | for key in key_from_image_info(image): 33 | built_tags.add(key) 34 | return built_tags 35 | 36 | 37 | def encode_tag(tag): 38 | return tag.replace('/', '-') 39 | 40 | 41 | def tag_image(client, image, new_ref): 42 | tag = encode_tag(new_ref) 43 | old_image = image.name + ':' + image.ref 44 | repository = image.name 45 | evt = { 46 | 'event': 'tag', 47 | 'old_image': old_image, 48 | 'repository': repository, 49 | 'tag': tag, 50 | } 51 | try: 52 | client.tag( 53 | old_image, 54 | repository, 55 | tag=tag, 56 | force=True, 57 | ) 58 | except d_errors.NotFound: 59 | message = 'Error tagging {}, not found'.format(old_image) 60 | evt.update(error(message)) 61 | 62 | return evt 63 | 64 | 65 | def error(message): 66 | return { 67 | 'error': message, 68 | 'errorDetail': {'message': message}, 69 | } 70 | 71 | 72 | def warn(message): 73 | return { 74 | 'warn': message, 75 | 'errorDetail': {'message': message}, 76 | } 77 | -------------------------------------------------------------------------------- /shipwright/_lib/image.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import json 4 | import os 5 | import re 6 | from collections import namedtuple 7 | 8 | Image = namedtuple( 9 | 'Image', 10 | ['name', 'dir_path', 'path', 'parent', 'short_name', 'copy_paths'], 11 | ) 12 | 13 | JSONDecodeError = getattr(json, 'JSONDecodeError', ValueError) 14 | 15 | 16 | def list_images(namespace, name_map, root_path): 17 | images = [] 18 | for path in build_files(root_path): 19 | name, short_name = image_name(namespace, name_map, root_path, path) 20 | images.append(Image( 21 | name=name, 22 | short_name=short_name, 23 | dir_path=os.path.dirname(path), 24 | copy_paths=copy_paths(path), 25 | path=path, 26 | parent=parent(path), 27 | )) 28 | return images 29 | 30 | 31 | def image_name(namespace, name_map, root_path, path): 32 | """ 33 | Determines the name of the image from the config file 34 | or based on it's parent directory name. 35 | 36 | >>> image_name( 37 | ... 'shipwright', {'blah':'foo/blah'}, 'x/', 'x/blah/Dockerfile', 38 | ... ) 39 | ('foo/blah', 'foo/blah') 40 | 41 | >>> image_name( 42 | ... 'shipwright', {'blah':'foo/blah'}, 'x/', 'x/baz/Dockerfile' 43 | ... ) 44 | ('shipwright/baz', 'baz') 45 | 46 | """ 47 | 48 | if path.startswith(root_path): 49 | relative_path = os.path.dirname(path[len(root_path):]) 50 | docker_repo = name_map.get(relative_path) 51 | 52 | if docker_repo is not None: 53 | return docker_repo, docker_repo 54 | short_name = name(path) 55 | # try to guess the name from the path 56 | return namespace + '/' + short_name, short_name 57 | 58 | 59 | # path -> iter([path ... / Dockerfile, ... ]) 60 | def build_files(build_root): 61 | """ 62 | Given a directory returns an iterator where each item is 63 | a path to a dockerfile 64 | 65 | Setup creates 3 dockerfiles under test root along with other 66 | files 67 | 68 | >>> test_root = getfixture('tmpdir').mkdir('images') 69 | >>> test_root.mkdir('image1').join('Dockerfile').write('FROM ubuntu') 70 | >>> test_root.mkdir('image2').join('Dockerfile').write('FROM ubuntu') 71 | >>> image3 = test_root.mkdir('image3') 72 | >>> image3.join('Dockerfile').write('FROM ubuntu') 73 | >>> image3.join('Dockerfile-dev').write('FROM ubuntu') 74 | >>> other = test_root.mkdir('other') 75 | >>> _ = other.mkdir('subdir1') 76 | >>> other.mkdir('subdir2').join('empty.txt').write('') 77 | 78 | >>> files = build_files(str(test_root)) 79 | >>> sorted(files) # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE 80 | ['.../image1/Dockerfile', '.../image2/Dockerfile', 81 | '.../image3/Dockerfile', '.../image3/Dockerfile-dev'] 82 | 83 | """ 84 | for root, dirs, files in os.walk(build_root): 85 | for filename in files: 86 | if filename.startswith('Dockerfile'): 87 | yield os.path.join(root, filename) 88 | 89 | 90 | # path -> str 91 | def name(docker_path): 92 | """ 93 | Return the immediate directory of a path pointing to a dockerfile. 94 | Raises ValueError if the path does not end in Dockerfile 95 | 96 | >>> name('/blah/foo/Dockerfile') 97 | 'foo' 98 | 99 | >>> name('/blah/foo/Dockerfile-dev') 100 | 'foo-dev' 101 | 102 | >>> name('/blah/foo/not-a-Dockerfile-dev') 103 | Traceback (most recent call last): 104 | ... 105 | ValueError: '/blah/foo/not-a-Dockerfile-dev' is not a valid Dockerfile 106 | 107 | >>> name('/blah/foo/setup.py') 108 | Traceback (most recent call last): 109 | ... 110 | ValueError: '/blah/foo/setup.py' is not a valid Dockerfile 111 | """ 112 | 113 | filename = os.path.basename(docker_path) 114 | before, dockerfile, after = filename.partition('Dockerfile') 115 | if dockerfile != 'Dockerfile' or before != '': 116 | raise ValueError( 117 | "'{}' is not a valid Dockerfile".format(docker_path), 118 | ) 119 | 120 | return os.path.basename(os.path.dirname(docker_path)) + after 121 | 122 | 123 | def sub_if(ex, repl, string, flags): 124 | """ 125 | Attempt a substitutuion and return the substituted string if there were 126 | matches. 127 | """ 128 | res = re.sub(ex, repl, string, count=1, flags=flags) 129 | if res != string: 130 | return res 131 | 132 | 133 | def parse_copy(cmd): 134 | """ 135 | Parse the source directoreis from a docker COPY or ADD command 136 | 137 | Ignores http or ftp URLs in ADD commands 138 | """ 139 | copy = sub_if(r'^\s*(COPY)\s', '', cmd, re.I) 140 | add = sub_if(r'^\s*(ADD)\s', '', cmd, re.I) 141 | 142 | copy_cmd = copy or add 143 | if not copy_cmd: 144 | return [] 145 | 146 | try: 147 | result = json.loads(copy_cmd) 148 | except JSONDecodeError: 149 | result = None 150 | 151 | if not isinstance(result, list): 152 | result = copy_cmd.split(' ') 153 | 154 | paths = result[:-1] 155 | if not add: 156 | return paths 157 | 158 | return [p for p in paths if not re.match('(https?|ftp):', p, re.I)] 159 | 160 | 161 | def copy_paths(docker_path): 162 | dirname = os.path.dirname(docker_path) 163 | 164 | def join(path): 165 | return os.path.join(dirname, path) 166 | 167 | def copy_paths_gen(): 168 | yield docker_path 169 | yield join('.dockerignore') 170 | 171 | with open(docker_path) as f: 172 | for l in f: 173 | for p in parse_copy(l): 174 | yield os.path.normpath(join(p)) 175 | 176 | return frozenset(copy_paths_gen()) 177 | 178 | 179 | def parent(docker_path): 180 | """ 181 | >>> path = getfixture('tmpdir').mkdir('parent').join('Dockerfile') 182 | >>> path.write('FrOm ubuntu') 183 | 184 | >>> parent(str(path)) 185 | 'ubuntu' 186 | 187 | """ 188 | for l in open(docker_path): 189 | if l.strip().lower().startswith('from'): 190 | return l.split()[1] 191 | -------------------------------------------------------------------------------- /shipwright/_lib/msg.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from collections import namedtuple 4 | 5 | 6 | """ 7 | classes for communicating between generators 8 | """ 9 | 10 | 11 | class Message(object): 12 | """ 13 | Base class for all other messages 14 | """ 15 | 16 | 17 | class BuildComplete(Message, namedtuple('BuildComplete', ['target'])): 18 | """ 19 | A message to signify that the build has finished for a target. 20 | 21 | The build may not have completed succesfully 22 | """ 23 | -------------------------------------------------------------------------------- /shipwright/_lib/push.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import itertools 4 | import time 5 | 6 | from . import compat, docker 7 | 8 | 9 | def do_push(client, images): 10 | push_results = [push(client, image) for image in images] 11 | for evt in itertools.chain.from_iterable(push_results): 12 | yield evt 13 | 14 | 15 | def retry_gen(fn, max_attempts=3): 16 | for i in range(max_attempts): 17 | try: 18 | for v in fn(): 19 | yield v 20 | return 21 | except Exception as e: 22 | msg = 'Exception: {}, Attempt: {}'.format(e, i) 23 | yield docker.warn(msg) 24 | time.sleep(0.6 * (2 ** i)) 25 | 26 | for v in fn(): 27 | yield v 28 | 29 | 30 | def push(client, image_tag): 31 | image, tag = image_tag 32 | extra = {'event': 'push', 'image': image} 33 | 34 | def _push(): 35 | return client.push(image, tag, stream=True) 36 | 37 | for evt in retry_gen(_push): 38 | d = compat.json_loads(evt) 39 | d.update(extra) 40 | yield d 41 | -------------------------------------------------------------------------------- /shipwright/_lib/registry.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | 4 | class RegistryException(Exception): 5 | pass 6 | 7 | 8 | class Registry(object): 9 | def __init__(self, registries): 10 | self._registries = registries 11 | 12 | def _get_registry_and_repo(self, name): 13 | registry_name, success, repository = name.partition('/') 14 | if not (registry_name or success or repository): 15 | raise RegistryException('name must be of the form registry/name') 16 | 17 | return self._registries[registry_name], repository 18 | 19 | def get_manifest(self, name, tag): 20 | client, repository = self._get_registry_and_repo(name) 21 | return client.get_manifest(repository, tag) 22 | 23 | def put_manifest(self, name, tag, manifest): 24 | client, repository = self._get_registry_and_repo(name) 25 | return client.put_manifest(repository, tag, manifest) 26 | -------------------------------------------------------------------------------- /shipwright/_lib/source_control.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import binascii 4 | import fnmatch 5 | import hashlib 6 | import os.path 7 | import re 8 | from collections import namedtuple 9 | 10 | import git 11 | 12 | from . import image 13 | 14 | 15 | class Mode(object): 16 | pass 17 | 18 | 19 | AUTO = Mode() 20 | GIT = Mode() 21 | 22 | 23 | SOURCE_CONTROL = [ 24 | ('.git', GIT), 25 | ] 26 | 27 | 28 | class SourceControlNotFound(Exception): 29 | def __init__(self): 30 | possible_values = ', '.join([x for x, _ in SOURCE_CONTROL]) 31 | msg = 'Cannot find directory in {}'.format(possible_values) 32 | super(SourceControlNotFound, self).__init__(msg) 33 | 34 | 35 | def _last_commit(repo, paths): 36 | commits = repo.iter_commits( 37 | paths=paths, 38 | max_count=1, 39 | topo_order=True, 40 | ) 41 | for commit in commits: 42 | return commit 43 | 44 | 45 | _Target = namedtuple('Target', ['image', 'ref', 'children']) 46 | 47 | 48 | class Target(_Target): 49 | @property 50 | def name(self): 51 | return self.image.name 52 | 53 | @property 54 | def short_name(self): 55 | return self.image.short_name 56 | 57 | @property 58 | def parent(self): 59 | return self.image.parent 60 | 61 | @property 62 | def path(self): 63 | return self.image.path 64 | 65 | 66 | del _Target 67 | 68 | 69 | def _image_parents(index, image): 70 | while image: 71 | yield image 72 | image = index.get(image.parent) 73 | 74 | 75 | def _hexsha(ref): 76 | if ref is not None: 77 | return ref.hexsha[:12] 78 | else: 79 | return 'g' * 12 80 | 81 | 82 | def _hash_blob(blob): 83 | if blob.hexsha != blob.NULL_HEX_SHA: 84 | return blob.hexsha 85 | else: 86 | return blob.repo.git.hash_object(blob.abspath) 87 | 88 | 89 | def _hash_blobs(blobs): 90 | return [(b.path, _hash_blob(b)) for b in blobs if b] 91 | 92 | 93 | def _abspath(repo_wd, path): 94 | return os.path.abspath(os.path.join(repo_wd, path)) 95 | 96 | 97 | def _in_paths(repo_wd, base_paths, path): 98 | wd = repo_wd 99 | p = _abspath(repo_wd, path) 100 | 101 | for base_path in base_paths: 102 | path_pattern = _abspath(wd, base_path) 103 | if not re.match('[*?]', path_pattern): 104 | if p == path_pattern: 105 | return True 106 | extra = '*' if path_pattern.endswith(os.sep) else os.sep + '*' 107 | path_pattern += extra 108 | 109 | if fnmatch.fnmatch(p, path_pattern): 110 | return True 111 | return False 112 | 113 | 114 | def _dirty_suffix(repo, base_paths=['.']): 115 | repo_wd = repo.working_dir 116 | diff = repo.head.commit.diff(None) 117 | a_hashes = _hash_blobs(d.a_blob for d in diff) 118 | b_hashes = _hash_blobs(d.b_blob for d in diff) 119 | 120 | u_files = repo.untracked_files 121 | untracked_hashes = [(path, repo.git.hash_object(path)) for path in u_files] 122 | 123 | hashes = sorted(a_hashes) + sorted(b_hashes + untracked_hashes) 124 | filtered_hashes = [ 125 | (path, h) for path, h in hashes if _in_paths(repo_wd, base_paths, path) 126 | ] 127 | 128 | if not filtered_hashes: 129 | return '' 130 | 131 | digest = hashlib.sha256() 132 | for path, h in filtered_hashes: 133 | digest.update(path.encode('utf-8') + b'\0' + h.encode('utf-8')) 134 | return '-dirty-' + binascii.hexlify(digest.digest())[:12].decode('utf-8') 135 | 136 | 137 | class SourceControl(object): 138 | pass 139 | 140 | 141 | class GitSourceControl(SourceControl): 142 | mode = GIT 143 | 144 | def __init__(self, path, namespace, name_map): 145 | self.path = path 146 | self._namespace = namespace 147 | self._name_map = name_map 148 | self._repo = git.Repo(path) 149 | 150 | def is_dirty(self): 151 | repo = self._repo 152 | return bool(repo.untracked_files or repo.head.commit.diff(None)) 153 | 154 | def default_tags(self): 155 | repo = self._repo 156 | if repo.head.is_detached: 157 | return [] 158 | 159 | branch = repo.active_branch.name 160 | return [branch] 161 | 162 | def this_ref_str(self): 163 | return _hexsha(self._repo.commit()) + _dirty_suffix(self._repo) 164 | 165 | def targets(self): 166 | repo = self._repo 167 | 168 | images = image.list_images( 169 | self._namespace, 170 | self._name_map, 171 | self.path, 172 | ) 173 | c_index = {c.name: c for c in images} 174 | 175 | targets = [] 176 | 177 | for c in images: 178 | paths = sorted(frozenset.union( 179 | *(p.copy_paths for p in _image_parents(c_index, c)) 180 | )) 181 | ref = (_hexsha(_last_commit(repo, paths)) + 182 | _dirty_suffix(repo, paths)) 183 | targets.append(Target(image=c, ref=ref, children=None)) 184 | 185 | return targets 186 | 187 | 188 | def get_mode(path): 189 | for scm_dir, mode in SOURCE_CONTROL: 190 | if os.path.isdir(os.path.join(path, scm_dir)): 191 | return mode 192 | raise SourceControlNotFound() 193 | 194 | 195 | def source_control(path, namespace, name_map, mode=None): 196 | if mode is None: 197 | mode = AUTO 198 | assert isinstance(mode, Mode) 199 | the_mode = mode if mode is not AUTO else get_mode(path) 200 | for cls in SourceControl.__subclasses__(): 201 | if cls.mode is the_mode: 202 | return cls(path, namespace, name_map) 203 | -------------------------------------------------------------------------------- /shipwright/_lib/tar.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import io 4 | import os 5 | import re 6 | import tarfile 7 | from os.path import join 8 | 9 | from docker import utils 10 | 11 | RE_DOCKER_TAG = re.compile( 12 | r'^(?P\s*from\s+)' 13 | r'(?P[\w.-]+(?P:\d+)?(?P([\w.-]+/)+|/))?' 14 | r'(?P[\w.-]+)' 15 | r'(?P(\s*))$', 16 | flags=re.MULTILINE | re.IGNORECASE, 17 | ) 18 | 19 | 20 | def bundle_docker_dir(tag, docker_path): 21 | """ 22 | Tars up a directory using the normal docker.util.tar method but 23 | adds tag if it's not None. 24 | """ 25 | 26 | # tar up the directory minus the Dockerfile, 27 | 28 | path = os.path.dirname(docker_path) 29 | try: 30 | with open(join(path, '.dockerignore')) as f: 31 | stripped = (p.strip() for p in f.readlines()) 32 | ignore = [x for x in stripped if x] 33 | except IOError: 34 | ignore = [] 35 | 36 | dockerfile_name = os.path.basename(docker_path) 37 | 38 | if tag is None: 39 | return utils.tar(path, ignore, dockerfile=dockerfile_name) 40 | 41 | # docker-py 1.6+ won't ignore the dockerfile 42 | # passing dockerfile='' works around the issuen 43 | # and lets us add the modified file when we're done. 44 | ignore.append(dockerfile_name) 45 | fileobj = utils.tar(path, ignore, dockerfile='') 46 | 47 | t = tarfile.open(fileobj=fileobj, mode='a') 48 | dfinfo = tarfile.TarInfo(dockerfile_name) 49 | 50 | contents = tag_parent(tag, open(join(path, dockerfile_name)).read()) 51 | if not isinstance(contents, bytes): 52 | contents = contents.encode('utf8') 53 | dockerfile = io.BytesIO(contents) 54 | 55 | dfinfo.size = len(dockerfile.getvalue()) 56 | t.addfile(dfinfo, dockerfile) 57 | t.close() 58 | fileobj.seek(0) 59 | return fileobj 60 | 61 | 62 | def tag_parent(tag, docker_content): 63 | r""" 64 | Replace the From clause like 65 | 66 | FROM somerepo/image 67 | 68 | To 69 | 70 | somerepo/image:tag 71 | 72 | 73 | >>> print(tag_parent( 74 | ... "blah", 75 | ... "# comment\n" 76 | ... "author bob barker\n" 77 | ... "FroM somerepo/image\n\n" 78 | ... "RUN echo hi mom\n" 79 | ... )) 80 | # comment 81 | author bob barker 82 | FroM somerepo/image:blah 83 | 84 | RUN echo hi mom 85 | 86 | 87 | 88 | >>> print(tag_parent( 89 | ... "blah", 90 | ... "# comment\n" 91 | ... "author bob barker\n" 92 | ... "FroM localhost:5000/somerepo/image\n\n" 93 | ... "RUN echo hi mom\n" 94 | ... )) 95 | # comment 96 | author bob barker 97 | FroM localhost:5000/somerepo/image:blah 98 | 99 | RUN echo hi mom 100 | 101 | 102 | >>> print(tag_parent( 103 | ... "blah", 104 | ... "# comment\n" 105 | ... "author bob barker\n" 106 | ... "FroM localhost:5000/somerepoimage\n\n" 107 | ... "RUN echo hi mom\n" 108 | ... )) 109 | # comment 110 | author bob barker 111 | FroM localhost:5000/somerepoimage:blah 112 | 113 | RUN echo hi mom 114 | 115 | 116 | >>> print(tag_parent( 117 | ... "blah", 118 | ... "# comment\n" 119 | ... "author bob barker\n" 120 | ... "FroM docker.example.com:5000/somerepo/image\n\n" 121 | ... "RUN echo hi mom\n", 122 | ... )) 123 | # comment 124 | author bob barker 125 | FroM docker.example.com:5000/somerepo/image:blah 126 | 127 | RUN echo hi mom 128 | 129 | """ 130 | 131 | return RE_DOCKER_TAG.sub( 132 | r'\g\g\g:{tag}\g'.format(tag=tag), 133 | docker_content, 134 | ) 135 | 136 | 137 | # str -> str -> fileobj 138 | def mkcontext(tag, docker_path): 139 | """ 140 | Returns a streaming tarfile suitable for passing to docker build. 141 | 142 | This method expects that there will be a Dockerfile in the same 143 | directory as path. The contents of which will be substituted 144 | with a tag that ensures that the image depends on a parent built 145 | within the same build_ref (bulid group) as the image being built. 146 | """ 147 | 148 | return bundle_docker_dir( 149 | tag, 150 | docker_path, 151 | ) 152 | -------------------------------------------------------------------------------- /shipwright/_lib/targets.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import json 4 | import os 5 | 6 | from . import dependencies, source_control 7 | 8 | 9 | def targets(path='.', upto=None, tags=None, mode=None): 10 | if upto is None: 11 | upto = [] 12 | try: 13 | config = json.load(open( 14 | os.path.join(path, '.shipwright.json'), 15 | )) 16 | except IOError: 17 | config = { 18 | 'namespace': os.environ.get('SW_NAMESPACE'), 19 | } 20 | 21 | namespace = config['namespace'] 22 | name_map = config.get('names', {}) 23 | 24 | scm = source_control.source_control(path, namespace, name_map, mode) 25 | targets = dependencies.eval( 26 | { 27 | 'upto': upto, 28 | 'exact': [], 29 | 'dependents': [], 30 | 'exclude': [], 31 | }, 32 | scm.targets(), 33 | ) 34 | this_ref_str = scm.this_ref_str() 35 | default_tags = scm.default_tags() 36 | all_tags = ( 37 | (tags if tags is not None else []) + 38 | default_tags + 39 | [this_ref_str] 40 | ) 41 | 42 | def _targets(): 43 | for image in targets: 44 | yield image.name + ':' + image.ref 45 | for tag in all_tags: 46 | yield image.name + ':' + tag.replace('/', '-') 47 | 48 | return list(_targets()) 49 | -------------------------------------------------------------------------------- /shipwright/_lib/zipper.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from collections import namedtuple 4 | 5 | Path = namedtuple('Path', 'l, r, pnodes, ppath, changed') 6 | 7 | 8 | def zipper(root, is_branch, children, make_node): 9 | return Loc(root, None, is_branch, children, make_node) 10 | 11 | 12 | _Loc = namedtuple( 13 | 'Loc', 14 | ['current', 'path', 'is_branch', 'get_children', 'make_node'], 15 | ) 16 | 17 | 18 | class Loc(_Loc): 19 | 20 | def node(self): 21 | return self.current 22 | 23 | def children(self): 24 | if self.branch(): 25 | return self.get_children(self.current) 26 | 27 | def branch(self): 28 | return self.is_branch(self.current) 29 | 30 | def at_end(self): 31 | return not bool(self.path) 32 | 33 | def down(self): 34 | children = self.children() 35 | if children: 36 | extra = (self.current,) 37 | pnodes = self.path.pnodes + extra if self.path else extra 38 | path = Path( 39 | l=(), 40 | r=children[1:], 41 | pnodes=pnodes, 42 | ppath=self.path, 43 | changed=False, 44 | ) 45 | 46 | return self._replace(current=children[0], path=path) 47 | 48 | def up(self): 49 | if self.path: 50 | l, r, pnodes, ppath, changed = self.path 51 | if pnodes: 52 | pnode = pnodes[-1] 53 | if changed: 54 | return self._replace( 55 | current=self.make_node(pnode, l + (self.current,) + r), 56 | path=ppath and ppath._replace(changed=True), 57 | ) 58 | else: 59 | return self._replace(current=pnode, path=ppath) 60 | 61 | def top(self): 62 | loc = self 63 | while loc.path: 64 | loc = loc.up() 65 | return loc 66 | 67 | def right(self): 68 | if self.path and self.path.r: 69 | l, rs = self.path[:2] 70 | current, rnext = rs[0], rs[1:] 71 | return self._replace(current=current, path=self.path._replace( 72 | l=l + (self.current,), 73 | r=rnext, 74 | )) 75 | 76 | def leftmost_descendant(self): 77 | loc = self 78 | while loc.branch(): 79 | d = loc.down() 80 | if d: 81 | loc = d 82 | else: 83 | break 84 | 85 | return loc 86 | 87 | def _rightmost(self): 88 | """Returns the right most sibling at this location or self""" 89 | 90 | path = self.path 91 | if path: 92 | l, r = self.path[:2] 93 | t = l + (self.current,) + r 94 | current = t[-1] 95 | 96 | return self._replace(current=current, path=path._replace( 97 | l=t[:-1], 98 | r=(), 99 | )) 100 | else: 101 | return self 102 | 103 | def _rightmost_descendant(self): 104 | loc = self 105 | while loc.branch(): 106 | d = loc.down() 107 | if d: 108 | loc = d._rightmost() 109 | else: 110 | break 111 | return loc 112 | 113 | def postorder_next(self): 114 | """ 115 | Visit's nodes in depth-first post-order. 116 | 117 | For eaxmple given the following tree: 118 | 119 | a 120 | / \ 121 | b e 122 | ^ ^ 123 | c d f g 124 | 125 | postorder next will visit the nodes in the following order 126 | c, d, b, f, g, e a 127 | 128 | 129 | Note this method ends when it reaches the root node. To 130 | start traversal from the root call leftmost_descendant() 131 | first. See postorder_iter for an example. 132 | 133 | """ 134 | 135 | r = self.right() 136 | if (r): 137 | return r.leftmost_descendant() 138 | else: 139 | return self.up() 140 | 141 | def edit(self, f, *args): 142 | """Replace the node at this loc with the value of f(node, *args)""" 143 | return self.replace(f(self.current, *args)) 144 | 145 | def insert(self, item): 146 | """ 147 | Inserts the item as the leftmost child of the node at this loc, 148 | without moving. 149 | """ 150 | return self.replace( 151 | self.make_node(self.node(), (item,) + self.children()), 152 | ) 153 | 154 | def replace(self, value): 155 | if self.path: 156 | return self._replace( 157 | current=value, 158 | path=self.path._replace(changed=True), 159 | ) 160 | else: 161 | return self._replace(current=value) 162 | 163 | def find(self, func): 164 | loc = self.leftmost_descendant() 165 | while True: 166 | if func(loc): 167 | return loc 168 | elif loc.at_end(): 169 | return None 170 | else: 171 | loc = loc.postorder_next() 172 | 173 | def remove(self): 174 | """ 175 | Removes the node at the current location, returning the 176 | node that would have proceeded it in a depth-first walk. 177 | 178 | For eaxmple given the following tree: 179 | 180 | a 181 | / \ 182 | b e 183 | ^ ^ 184 | c d f g 185 | ^ 186 | c1 c2 187 | Removing c would return b, removing d would return c2. 188 | 189 | 190 | """ 191 | path = self.path 192 | if not path: 193 | raise IndexError('Remove at top') 194 | 195 | l, r, pnodes, ppath, changed = path 196 | 197 | if l: 198 | ls, current = l[:-1], l[-1] 199 | return self._replace(current=current, path=path._replace( 200 | l=ls, 201 | changed=True, 202 | ))._rightmost_descendant() 203 | 204 | else: 205 | return self._replace( 206 | current=self.make_node(pnodes[-1], r), 207 | path=ppath and ppath._replace(changed=True), 208 | ) 209 | 210 | 211 | del _Loc 212 | -------------------------------------------------------------------------------- /shipwright/exceptions.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from ._lib.source_control import SourceControlNotFound 4 | 5 | __all__ = ['SourceControlNotFound'] 6 | -------------------------------------------------------------------------------- /shipwright/targets.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from ._lib.source_control import AUTO, GIT 4 | from ._lib.targets import targets 5 | 6 | __all__ = ['targets', 'AUTO', 'GIT'] 7 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import docker 4 | import pytest 5 | from docker import utils as docker_utils 6 | 7 | 8 | @pytest.fixture(scope='session') 9 | def docker_client(): 10 | client_cfg = docker_utils.kwargs_from_env() 11 | return docker.APIClient(version='1.21', **client_cfg) 12 | 13 | 14 | @pytest.yield_fixture(scope='session') 15 | def registry(docker_client): 16 | cli = docker_client 17 | cli.pull('registry', '2') 18 | cont = cli.create_container( 19 | 'registry:2', 20 | ports=[5000], 21 | host_config=cli.create_host_config( 22 | port_bindings={ 23 | 5000: 5000, 24 | }, 25 | ), 26 | ) 27 | try: 28 | cli.start(cont) 29 | try: 30 | yield 31 | finally: 32 | cli.stop(cont) 33 | finally: 34 | cli.remove_container(cont, v=True, force=True) 35 | -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/6si/shipwright/f6fb93f82a83ddb856ab1a421c382302ac2e2e19/tests/integration/__init__.py -------------------------------------------------------------------------------- /tests/integration/examples/failing-build/.shipwright.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1.0, 3 | 4 | "namespace": "shipwright" 5 | 6 | } 7 | -------------------------------------------------------------------------------- /tests/integration/examples/failing-build/base/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM busybox 2 | ADD . /code 3 | -------------------------------------------------------------------------------- /tests/integration/examples/failing-build/base/base.txt: -------------------------------------------------------------------------------- 1 | Hi mom 2 | -------------------------------------------------------------------------------- /tests/integration/examples/failing-build/crashy-from/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM example.invalid/invalid 2 | COPY . /code 3 | -------------------------------------------------------------------------------- /tests/integration/examples/failing-build/crashy-run/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM shipwright/base 2 | RUN exit 9000 3 | -------------------------------------------------------------------------------- /tests/integration/examples/failing-build/works/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM shipwright/base 2 | ADD . /code 3 | -------------------------------------------------------------------------------- /tests/integration/examples/multi-dockerfile/.shipwright.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1.0, 3 | 4 | "namespace": "shipwright" 5 | 6 | } 7 | -------------------------------------------------------------------------------- /tests/integration/examples/multi-dockerfile/base/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | ADD . /code 3 | -------------------------------------------------------------------------------- /tests/integration/examples/multi-dockerfile/base/base.txt: -------------------------------------------------------------------------------- 1 | Hi mom 2 | -------------------------------------------------------------------------------- /tests/integration/examples/multi-dockerfile/service1/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM shipwright/base 2 | ADD ./src /code/ 3 | -------------------------------------------------------------------------------- /tests/integration/examples/multi-dockerfile/service1/Dockerfile-dev: -------------------------------------------------------------------------------- 1 | FROM shipwright/service1 2 | ADD ./test /code 3 | -------------------------------------------------------------------------------- /tests/integration/examples/multi-dockerfile/service1/src/foo.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/6si/shipwright/f6fb93f82a83ddb856ab1a421c382302ac2e2e19/tests/integration/examples/multi-dockerfile/service1/src/foo.js -------------------------------------------------------------------------------- /tests/integration/examples/multi-dockerfile/service1/test/test_foo.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/6si/shipwright/f6fb93f82a83ddb856ab1a421c382302ac2e2e19/tests/integration/examples/multi-dockerfile/service1/test/test_foo.js -------------------------------------------------------------------------------- /tests/integration/examples/shipwright-localhost-sample/.shipwright.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1.0, 3 | "namespace": "localhost:5000" 4 | } 5 | -------------------------------------------------------------------------------- /tests/integration/examples/shipwright-localhost-sample/base/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM busybox 2 | ADD . /code 3 | -------------------------------------------------------------------------------- /tests/integration/examples/shipwright-localhost-sample/base/base.txt: -------------------------------------------------------------------------------- 1 | Hi mom 2 | -------------------------------------------------------------------------------- /tests/integration/examples/shipwright-localhost-sample/service1/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM localhost:5000/shared 2 | ADD . /code 3 | -------------------------------------------------------------------------------- /tests/integration/examples/shipwright-localhost-sample/shared/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM localhost:5000/base 2 | ADD . /code 3 | -------------------------------------------------------------------------------- /tests/integration/examples/shipwright-sample/.shipwright.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1.0, 3 | 4 | "namespace": "shipwright" 5 | 6 | } 7 | -------------------------------------------------------------------------------- /tests/integration/examples/shipwright-sample/base/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM busybox 2 | ADD . /code 3 | -------------------------------------------------------------------------------- /tests/integration/examples/shipwright-sample/base/base.txt: -------------------------------------------------------------------------------- 1 | Hi mom 2 | -------------------------------------------------------------------------------- /tests/integration/examples/shipwright-sample/service1/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM shipwright/shared 2 | ADD . /code 3 | -------------------------------------------------------------------------------- /tests/integration/examples/shipwright-sample/shared/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM shipwright/base 2 | ADD . /code 3 | -------------------------------------------------------------------------------- /tests/integration/test_docker_builds.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import json 4 | 5 | import pkg_resources 6 | import pytest 7 | from docker import utils as docker_utils 8 | 9 | from shipwright._lib import cli as shipw_cli 10 | 11 | from .utils import commit_untracked, create_repo, default_args, get_defaults 12 | 13 | 14 | def test_sample(tmpdir, docker_client): 15 | path = str(tmpdir.join('shipwright-sample')) 16 | source = pkg_resources.resource_filename( 17 | __name__, 18 | 'examples/shipwright-sample', 19 | ) 20 | repo = create_repo(path, source) 21 | tag = repo.head.ref.commit.hexsha[:12] 22 | 23 | client_cfg = docker_utils.kwargs_from_env() 24 | cli = docker_client 25 | 26 | try: 27 | shipw_cli.run( 28 | path=path, 29 | client_cfg=client_cfg, 30 | arguments=get_defaults(), 31 | environ={}, 32 | ) 33 | 34 | service1, shared, base = ( 35 | cli.images(name='shipwright/service1') + 36 | cli.images(name='shipwright/shared') + 37 | cli.images(name='shipwright/base') 38 | ) 39 | 40 | assert set(service1['RepoTags']) == { 41 | 'shipwright/service1:master', 42 | 'shipwright/service1:latest', 43 | 'shipwright/service1:' + tag, 44 | } 45 | 46 | assert set(shared['RepoTags']) == { 47 | 'shipwright/shared:master', 48 | 'shipwright/shared:latest', 49 | 'shipwright/shared:' + tag, 50 | } 51 | 52 | assert set(base['RepoTags']) == { 53 | 'shipwright/base:master', 54 | 'shipwright/base:latest', 55 | 'shipwright/base:' + tag, 56 | } 57 | finally: 58 | old_images = ( 59 | cli.images(name='shipwright/service1', quiet=True) + 60 | cli.images(name='shipwright/shared', quiet=True) + 61 | cli.images(name='shipwright/base', quiet=True) 62 | ) 63 | for image in old_images: 64 | cli.remove_image(image, force=True) 65 | 66 | 67 | def test_multi_dockerfile(tmpdir, docker_client): 68 | path = str(tmpdir.join('shipwright-sample')) 69 | source = pkg_resources.resource_filename( 70 | __name__, 71 | 'examples/multi-dockerfile', 72 | ) 73 | repo = create_repo(path, source) 74 | tag = repo.head.ref.commit.hexsha[:12] 75 | 76 | client_cfg = docker_utils.kwargs_from_env() 77 | cli = docker_client 78 | 79 | try: 80 | shipw_cli.run( 81 | path=path, 82 | client_cfg=client_cfg, 83 | arguments=get_defaults(), 84 | environ={}, 85 | ) 86 | 87 | service1_dev, service1, base = ( 88 | cli.images(name='shipwright/service1-dev') + 89 | cli.images(name='shipwright/service1') + 90 | cli.images(name='shipwright/base') 91 | ) 92 | 93 | assert set(service1_dev['RepoTags']) == { 94 | 'shipwright/service1-dev:master', 95 | 'shipwright/service1-dev:latest', 96 | 'shipwright/service1-dev:' + tag, 97 | } 98 | 99 | assert set(service1['RepoTags']) == { 100 | 'shipwright/service1:master', 101 | 'shipwright/service1:latest', 102 | 'shipwright/service1:' + tag, 103 | } 104 | 105 | assert set(base['RepoTags']) == { 106 | 'shipwright/base:master', 107 | 'shipwright/base:latest', 108 | 'shipwright/base:' + tag, 109 | } 110 | finally: 111 | old_images = ( 112 | cli.images(name='shipwright/service1-dev', quiet=True) + 113 | cli.images(name='shipwright/service1', quiet=True) + 114 | cli.images(name='shipwright/base', quiet=True) 115 | ) 116 | for image in old_images: 117 | cli.remove_image(image, force=True) 118 | 119 | 120 | def test_clean_tree_avoids_rebuild(tmpdir, docker_client): 121 | tmp = tmpdir.join('shipwright-sample') 122 | path = str(tmp) 123 | source = pkg_resources.resource_filename( 124 | __name__, 125 | 'examples/shipwright-sample', 126 | ) 127 | repo = create_repo(path, source) 128 | old_tag = repo.head.ref.commit.hexsha[:12] 129 | 130 | client_cfg = docker_utils.kwargs_from_env() 131 | cli = docker_client 132 | 133 | try: 134 | 135 | shipw_cli.run( 136 | path=path, 137 | client_cfg=client_cfg, 138 | arguments=get_defaults(), 139 | environ={}, 140 | ) 141 | 142 | tmp.join('service1/base.txt').write('Hi mum') 143 | commit_untracked(repo) 144 | new_tag = repo.head.ref.commit.hexsha[:12] 145 | 146 | shipw_cli.run( 147 | path=path, 148 | client_cfg=client_cfg, 149 | arguments=get_defaults(), 150 | environ={}, 151 | ) 152 | 153 | service1a, service1b, shared, base = ( 154 | cli.images(name='shipwright/service1') + 155 | cli.images(name='shipwright/shared') + 156 | cli.images(name='shipwright/base') 157 | ) 158 | 159 | service1a, service1b = sorted( 160 | (service1a, service1b), 161 | key=lambda x: len(x['RepoTags']), 162 | reverse=True, 163 | ) 164 | 165 | assert set(service1a['RepoTags']) == { 166 | 'shipwright/service1:master', 167 | 'shipwright/service1:latest', 168 | 'shipwright/service1:' + new_tag, 169 | } 170 | 171 | assert set(service1b['RepoTags']) == { 172 | 'shipwright/service1:' + old_tag, 173 | } 174 | 175 | assert set(shared['RepoTags']) == { 176 | 'shipwright/shared:master', 177 | 'shipwright/shared:latest', 178 | 'shipwright/shared:' + old_tag, 179 | 'shipwright/shared:' + new_tag, 180 | } 181 | 182 | assert set(base['RepoTags']) == { 183 | 'shipwright/base:master', 184 | 'shipwright/base:latest', 185 | 'shipwright/base:' + old_tag, 186 | 'shipwright/base:' + new_tag, 187 | } 188 | 189 | finally: 190 | old_images = ( 191 | cli.images(name='shipwright/service1', quiet=True) + 192 | cli.images(name='shipwright/shared', quiet=True) + 193 | cli.images(name='shipwright/base', quiet=True) 194 | ) 195 | for image in old_images: 196 | cli.remove_image(image, force=True) 197 | 198 | 199 | def test_clean_tree_calcualted_via_copy_cmd_parsing(tmpdir, docker_client): 200 | tmp = tmpdir.join('shipwright-sample') 201 | path = str(tmp) 202 | repo = create_repo(path) 203 | tmp.join('.shipwright.json').write(json.dumps({ 204 | 'version': 1.0, 205 | 'namespace': 'shipwright', 206 | })) 207 | fe = tmp.ensure('frontend', dir=True) 208 | fe.join('yarn.lock').write('FAKE YARN LOCK!\n', 'a') 209 | fe.join('Dockerfile-base').write('FROM busybox\n', 'a') 210 | fe.join('Dockerfile-base').write('COPY yarn.lock /yarn.lock\n', 'a') 211 | fe.join('src.js').write("var $ = require('jquery')\n", 'a') 212 | fe.join('Dockerfile').write('FROM shipwright/frontend-base\n', 'a') 213 | fe.join('Dockerfile').write('COPY src.js /src.js\n', 'a') 214 | commit_untracked(repo) 215 | old_tag = repo.head.ref.commit.hexsha[:12] 216 | 217 | client_cfg = docker_utils.kwargs_from_env() 218 | cli = docker_client 219 | 220 | try: 221 | 222 | shipw_cli.run( 223 | path=path, 224 | client_cfg=client_cfg, 225 | arguments=get_defaults(), 226 | environ={}, 227 | ) 228 | 229 | fe = tmp.join('frontend/src.js') 230 | fe.write("'import react from 'react';") 231 | repo.index.add([str(fe)]) 232 | commit_untracked(repo) 233 | new_tag = repo.head.ref.commit.hexsha[:12] 234 | 235 | shipw_cli.run( 236 | path=path, 237 | client_cfg=client_cfg, 238 | arguments=get_defaults(), 239 | environ={}, 240 | ) 241 | 242 | frontend_a, frontend_b, frontend_base = ( 243 | cli.images(name='shipwright/frontend') + 244 | cli.images(name='shipwright/frontend-base') 245 | ) 246 | 247 | frontend_a, frontend_b = sorted( 248 | (frontend_a, frontend_b), 249 | key=lambda x: len(x['RepoTags']), 250 | reverse=True, 251 | ) 252 | 253 | assert set(frontend_a['RepoTags']) == { 254 | 'shipwright/frontend:master', 255 | 'shipwright/frontend:latest', 256 | 'shipwright/frontend:' + new_tag, 257 | } 258 | 259 | assert set(frontend_b['RepoTags']) == { 260 | 'shipwright/frontend:' + old_tag, 261 | } 262 | 263 | assert set(frontend_base['RepoTags']) == { 264 | 'shipwright/frontend-base:master', 265 | 'shipwright/frontend-base:latest', 266 | 'shipwright/frontend-base:' + old_tag, 267 | 'shipwright/frontend-base:' + new_tag, 268 | } 269 | 270 | finally: 271 | old_images = ( 272 | cli.images(name='shipwright/frontend', quiet=True) + 273 | cli.images(name='shipwright/frontend-base', quiet=True) 274 | ) 275 | for image in old_images: 276 | cli.remove_image(image, force=True) 277 | 278 | 279 | def test_clean_tree_avoids_rebuild_new_image_definition(tmpdir, docker_client): 280 | tmp = tmpdir.join('shipwright-sample') 281 | path = str(tmp) 282 | source = pkg_resources.resource_filename( 283 | __name__, 284 | 'examples/shipwright-sample', 285 | ) 286 | repo = create_repo(path, source) 287 | old_tag = repo.head.ref.commit.hexsha[:12] 288 | 289 | client_cfg = docker_utils.kwargs_from_env() 290 | cli = docker_client 291 | 292 | try: 293 | shipw_cli.run( 294 | path=path, 295 | client_cfg=client_cfg, 296 | arguments=get_defaults(), 297 | environ={}, 298 | ) 299 | 300 | dockerfile = """ 301 | FROM busybox 302 | MAINTAINER shipwright 303 | """ 304 | 305 | tmp.mkdir('service2').join('Dockerfile').write(dockerfile) 306 | commit_untracked(repo) 307 | new_tag = repo.head.ref.commit.hexsha[:12] 308 | 309 | shipw_cli.run( 310 | path=path, 311 | client_cfg=client_cfg, 312 | arguments=get_defaults(), 313 | environ={}, 314 | ) 315 | 316 | service2, service1, shared, base = ( 317 | cli.images(name='shipwright/service2') + 318 | cli.images(name='shipwright/service1') + 319 | cli.images(name='shipwright/shared') + 320 | cli.images(name='shipwright/base') 321 | ) 322 | 323 | assert set(service2['RepoTags']) == { 324 | 'shipwright/service2:master', 325 | 'shipwright/service2:latest', 326 | 'shipwright/service2:' + new_tag, 327 | } 328 | 329 | assert set(service1['RepoTags']) == { 330 | 'shipwright/service1:master', 331 | 'shipwright/service1:latest', 332 | 'shipwright/service1:' + old_tag, 333 | 'shipwright/service1:' + new_tag, 334 | } 335 | 336 | assert set(shared['RepoTags']) == { 337 | 'shipwright/shared:master', 338 | 'shipwright/shared:latest', 339 | 'shipwright/shared:' + old_tag, 340 | 'shipwright/shared:' + new_tag, 341 | } 342 | 343 | assert set(base['RepoTags']) == { 344 | 'shipwright/base:master', 345 | 'shipwright/base:latest', 346 | 'shipwright/base:' + old_tag, 347 | 'shipwright/base:' + new_tag, 348 | } 349 | 350 | finally: 351 | old_images = ( 352 | cli.images(name='shipwright/service2', quiet=True) + 353 | cli.images(name='shipwright/service1', quiet=True) + 354 | cli.images(name='shipwright/shared', quiet=True) + 355 | cli.images(name='shipwright/base', quiet=True) 356 | ) 357 | for image in old_images: 358 | cli.remove_image(image, force=True) 359 | 360 | 361 | def test_dump_file(tmpdir, docker_client): 362 | dump_file = tmpdir.join('dump.txt') 363 | tmp = tmpdir.join('shipwright-sample') 364 | path = str(tmp) 365 | source = pkg_resources.resource_filename( 366 | __name__, 367 | 'examples/shipwright-sample', 368 | ) 369 | create_repo(path, source) 370 | 371 | client_cfg = docker_utils.kwargs_from_env() 372 | 373 | cli = docker_client 374 | 375 | try: 376 | args = get_defaults() 377 | args['--dump-file'] = str(dump_file) 378 | shipw_cli.run( 379 | path=path, 380 | client_cfg=client_cfg, 381 | arguments=args, 382 | environ={}, 383 | ) 384 | 385 | assert ' : FROM busybox' in dump_file.read() 386 | finally: 387 | old_images = ( 388 | cli.images(name='shipwright/service1', quiet=True) + 389 | cli.images(name='shipwright/shared', quiet=True) + 390 | cli.images(name='shipwright/base', quiet=True) 391 | ) 392 | for image in old_images: 393 | cli.remove_image(image, force=True) 394 | 395 | 396 | def test_exclude(tmpdir, docker_client): 397 | path = str(tmpdir.join('shipwright-sample')) 398 | source = pkg_resources.resource_filename( 399 | __name__, 400 | 'examples/shipwright-sample', 401 | ) 402 | create_repo(path, source) 403 | client_cfg = docker_utils.kwargs_from_env() 404 | 405 | cli = docker_client 406 | 407 | args = get_defaults() 408 | args['--exclude'] = [ 409 | 'shipwright/service1', 410 | 'shipwright/shared', 411 | ] 412 | 413 | try: 414 | shipw_cli.run( 415 | path=path, 416 | client_cfg=client_cfg, 417 | arguments=args, 418 | environ={}, 419 | ) 420 | 421 | base, = ( 422 | cli.images(name='shipwright/service1') + 423 | cli.images(name='shipwright/shared') + 424 | cli.images(name='shipwright/base') 425 | ) 426 | 427 | assert 'shipwright/base:master' in base['RepoTags'] 428 | assert 'shipwright/base:latest' in base['RepoTags'] 429 | finally: 430 | old_images = ( 431 | cli.images(name='shipwright/base', quiet=True) 432 | ) 433 | for image in old_images: 434 | cli.remove_image(image, force=True) 435 | 436 | 437 | def test_exact(tmpdir, docker_client): 438 | path = str(tmpdir.join('shipwright-sample')) 439 | source = pkg_resources.resource_filename( 440 | __name__, 441 | 'examples/shipwright-sample', 442 | ) 443 | create_repo(path, source) 444 | client_cfg = docker_utils.kwargs_from_env() 445 | 446 | cli = docker_client 447 | 448 | args = get_defaults() 449 | args['--exact'] = [ 450 | 'shipwright/base', 451 | ] 452 | 453 | try: 454 | shipw_cli.run( 455 | path=path, 456 | client_cfg=client_cfg, 457 | arguments=args, 458 | environ={}, 459 | ) 460 | 461 | base, = ( 462 | cli.images(name='shipwright/service1') + 463 | cli.images(name='shipwright/shared') + 464 | cli.images(name='shipwright/base') 465 | ) 466 | assert 'shipwright/base:master' in base['RepoTags'] 467 | assert 'shipwright/base:latest' in base['RepoTags'] 468 | finally: 469 | old_images = ( 470 | cli.images(name='shipwright/base', quiet=True) 471 | ) 472 | for image in old_images: 473 | cli.remove_image(image, force=True) 474 | 475 | 476 | def test_dirty_fails_without_flag(tmpdir): 477 | tmp = tmpdir.join('shipwright-sample') 478 | path = str(tmp) 479 | source = pkg_resources.resource_filename( 480 | __name__, 481 | 'examples/shipwright-sample', 482 | ) 483 | create_repo(path, source) 484 | tmp.join('service1/base.txt').write('Some text') 485 | client_cfg = docker_utils.kwargs_from_env() 486 | 487 | args = get_defaults() 488 | 489 | result = shipw_cli.run( 490 | path=path, 491 | client_cfg=client_cfg, 492 | arguments=args, 493 | environ={}, 494 | ) 495 | assert '--dirty' in result 496 | assert 'Abort' in result 497 | 498 | 499 | def test_dirty_flag(tmpdir, docker_client): 500 | tmp = tmpdir.join('shipwright-sample') 501 | path = str(tmp) 502 | source = pkg_resources.resource_filename( 503 | __name__, 504 | 'examples/shipwright-sample', 505 | ) 506 | create_repo(path, source) 507 | tmp.join('service1/base.txt').write('Some text') 508 | client_cfg = docker_utils.kwargs_from_env() 509 | 510 | cli = docker_client 511 | 512 | args = default_args() 513 | args.dirty = True 514 | 515 | try: 516 | shipw_cli.run( 517 | path=path, 518 | client_cfg=client_cfg, 519 | arguments=get_defaults(), 520 | environ={}, 521 | new_style_args=args, 522 | ) 523 | 524 | service1, shared, base = ( 525 | cli.images(name='shipwright/service1') + 526 | cli.images(name='shipwright/shared') + 527 | cli.images(name='shipwright/base') 528 | ) 529 | 530 | assert 'shipwright/service1:latest' in service1['RepoTags'] 531 | assert 'shipwright/service1:master' in service1['RepoTags'] 532 | assert 'shipwright/shared:latest' in shared['RepoTags'] 533 | assert 'shipwright/shared:master' in shared['RepoTags'] 534 | assert 'shipwright/base:latest' in base['RepoTags'] 535 | assert 'shipwright/base:master' in base['RepoTags'] 536 | 537 | finally: 538 | old_images = ( 539 | cli.images(name='shipwright/service1', quiet=True) + 540 | cli.images(name='shipwright/shared', quiet=True) + 541 | cli.images(name='shipwright/base', quiet=True) 542 | ) 543 | for image in old_images: 544 | cli.remove_image(image, force=True) 545 | 546 | 547 | def test_exit_on_failure_but_build_completes(tmpdir, docker_client): 548 | path = str(tmpdir.join('failing-build')) 549 | source = pkg_resources.resource_filename( 550 | __name__, 551 | 'examples/failing-build', 552 | ) 553 | repo = create_repo(path, source) 554 | tag = repo.head.ref.commit.hexsha[:12] 555 | 556 | client_cfg = docker_utils.kwargs_from_env() 557 | cli = docker_client 558 | 559 | try: 560 | with pytest.raises(SystemExit): 561 | shipw_cli.run( 562 | path=path, 563 | client_cfg=client_cfg, 564 | arguments=get_defaults(), 565 | environ={}, 566 | ) 567 | 568 | base, works = ( 569 | cli.images(name='shipwright/base') + 570 | cli.images(name='shipwright/works') + 571 | cli.images(name='shipwright/crashy-from') + 572 | cli.images(name='shipwright/crashy-dev') 573 | ) 574 | 575 | assert set(base['RepoTags']) == { 576 | 'shipwright/base:master', 577 | 'shipwright/base:latest', 578 | 'shipwright/base:' + tag, 579 | } 580 | 581 | assert set(works['RepoTags']) == { 582 | 'shipwright/works:master', 583 | 'shipwright/works:latest', 584 | 'shipwright/works:' + tag, 585 | } 586 | 587 | finally: 588 | old_images = ( 589 | cli.images(name='shipwright/works', quiet=True) + 590 | cli.images(name='shipwright/base', quiet=True) 591 | ) 592 | for image in old_images: 593 | cli.remove_image(image, force=True) 594 | 595 | 596 | def test_short_name_target(tmpdir, docker_client): 597 | path = str(tmpdir.join('shipwright-sample')) 598 | source = pkg_resources.resource_filename( 599 | __name__, 600 | 'examples/shipwright-sample', 601 | ) 602 | repo = create_repo(path, source) 603 | tag = repo.head.ref.commit.hexsha[:12] 604 | 605 | client_cfg = docker_utils.kwargs_from_env() 606 | cli = docker_client 607 | 608 | try: 609 | defaults = get_defaults() 610 | defaults['--upto'] = ['shared'] 611 | shipw_cli.run( 612 | path=path, 613 | client_cfg=client_cfg, 614 | arguments=defaults, 615 | environ={}, 616 | ) 617 | 618 | shared, base = ( 619 | cli.images(name='shipwright/service1') + 620 | cli.images(name='shipwright/shared') + 621 | cli.images(name='shipwright/base') 622 | ) 623 | 624 | assert set(shared['RepoTags']) == { 625 | 'shipwright/shared:master', 626 | 'shipwright/shared:latest', 627 | 'shipwright/shared:' + tag, 628 | } 629 | 630 | assert set(base['RepoTags']) == { 631 | 'shipwright/base:master', 632 | 'shipwright/base:latest', 633 | 'shipwright/base:' + tag, 634 | } 635 | finally: 636 | old_images = ( 637 | cli.images(name='shipwright/shared', quiet=True) + 638 | cli.images(name='shipwright/base', quiet=True) 639 | ) 640 | for image in old_images: 641 | cli.remove_image(image, force=True) 642 | 643 | 644 | def test_child_inherits_parents_build_tag(tmpdir, docker_client): 645 | tmp = tmpdir.join('shipwright-sample') 646 | path = str(tmp) 647 | source = pkg_resources.resource_filename( 648 | __name__, 649 | 'examples/shipwright-sample', 650 | ) 651 | repo = create_repo(path, source) 652 | old_tag = repo.head.ref.commit.hexsha[:12] 653 | 654 | client_cfg = docker_utils.kwargs_from_env() 655 | cli = docker_client 656 | 657 | try: 658 | 659 | shipw_cli.run( 660 | path=path, 661 | client_cfg=client_cfg, 662 | arguments=get_defaults(), 663 | environ={}, 664 | ) 665 | 666 | tmp.join('shared/base.txt').write('Hi mum') 667 | commit_untracked(repo) 668 | new_tag = repo.head.ref.commit.hexsha[:12] 669 | 670 | # Currently service1 has not had any changes, and so naivly would not 671 | # need to be built, however because it's parent, shared has changes 672 | # it will need to be rebuilt with the parent's build tag. 673 | 674 | # Dockhand asks the question: What is the latest commit in this 675 | # directory, and all of this image's parents? 676 | 677 | shipw_cli.run( 678 | path=path, 679 | client_cfg=client_cfg, 680 | arguments=get_defaults(), 681 | environ={}, 682 | ) 683 | 684 | service1a, service1b, sharedA, sharedB, base = ( 685 | cli.images(name='shipwright/service1') + 686 | cli.images(name='shipwright/shared') + 687 | cli.images(name='shipwright/base') 688 | ) 689 | 690 | service1a, service1b = sorted( 691 | (service1a, service1b), 692 | key=lambda x: len(x['RepoTags']), 693 | reverse=True, 694 | ) 695 | 696 | sharedA, sharedB = sorted( 697 | (sharedA, sharedB), 698 | key=lambda x: len(x['RepoTags']), 699 | reverse=True, 700 | ) 701 | 702 | assert set(service1a['RepoTags']) == { 703 | 'shipwright/service1:master', 704 | 'shipwright/service1:latest', 705 | 'shipwright/service1:' + new_tag, 706 | } 707 | 708 | assert set(service1b['RepoTags']) == { 709 | 'shipwright/service1:' + old_tag, 710 | } 711 | 712 | assert set(sharedA['RepoTags']) == { 713 | 'shipwright/shared:master', 714 | 'shipwright/shared:latest', 715 | 'shipwright/shared:' + new_tag, 716 | } 717 | 718 | assert set(sharedB['RepoTags']) == { 719 | 'shipwright/shared:' + old_tag, 720 | } 721 | 722 | assert set(base['RepoTags']) == { 723 | 'shipwright/base:master', 724 | 'shipwright/base:latest', 725 | 'shipwright/base:' + old_tag, 726 | 'shipwright/base:' + new_tag, 727 | } 728 | 729 | finally: 730 | old_images = ( 731 | cli.images(name='shipwright/service1', quiet=True) + 732 | cli.images(name='shipwright/shared', quiet=True) + 733 | cli.images(name='shipwright/base', quiet=True) 734 | ) 735 | for image in old_images: 736 | cli.remove_image(image, force=True) 737 | 738 | 739 | def test_build_with_repo_digest(tmpdir, docker_client, registry): 740 | path = str(tmpdir.join('shipwright-localhost-sample')) 741 | source = pkg_resources.resource_filename( 742 | __name__, 743 | 'examples/shipwright-localhost-sample', 744 | ) 745 | repo = create_repo(path, source) 746 | tag = repo.head.ref.commit.hexsha[:12] 747 | 748 | client_cfg = docker_utils.kwargs_from_env() 749 | cli = docker_client 750 | 751 | defaults = get_defaults() 752 | defaults['push'] = True 753 | try: 754 | shipw_cli.run( 755 | path=path, 756 | client_cfg=client_cfg, 757 | arguments=defaults, 758 | environ={}, 759 | ) 760 | 761 | # Remove a build image: 762 | old_images = cli.images(name='localhost:5000/service1') 763 | for image in old_images: 764 | cli.remove_image(image['Id'], force=True) 765 | 766 | repo_digest = old_images[0]['RepoDigests'][0] 767 | # Pull it so it's missing a build tag, but has a RepoDigest 768 | cli.pull(repo_digest) 769 | 770 | shipw_cli.run( 771 | path=path, 772 | client_cfg=client_cfg, 773 | arguments=defaults, 774 | environ={}, 775 | ) 776 | 777 | service1a, service1b, shared, base = ( 778 | cli.images(name='localhost:5000/service1') + 779 | cli.images(name='localhost:5000/shared') + 780 | cli.images(name='localhost:5000/base') 781 | ) 782 | 783 | if service1b['RepoTags'] is None: 784 | service1b, service1a = service1a, service1b 785 | 786 | assert service1a['RepoTags'] is None 787 | 788 | assert set(service1b['RepoTags']) == { 789 | 'localhost:5000/service1:master', 790 | 'localhost:5000/service1:latest', 791 | 'localhost:5000/service1:' + tag, 792 | } 793 | 794 | assert set(shared['RepoTags']) == { 795 | 'localhost:5000/shared:master', 796 | 'localhost:5000/shared:latest', 797 | 'localhost:5000/shared:' + tag, 798 | } 799 | 800 | assert set(base['RepoTags']) == { 801 | 'localhost:5000/base:master', 802 | 'localhost:5000/base:latest', 803 | 'localhost:5000/base:' + tag, 804 | } 805 | finally: 806 | old_images = ( 807 | cli.images(name='localhost:5000/service1', quiet=True) + 808 | cli.images(name='localhost:5000/shared', quiet=True) + 809 | cli.images(name='localhost:5000/base', quiet=True) 810 | ) 811 | for image in old_images: 812 | cli.remove_image(image, force=True) 813 | 814 | 815 | def test_docker_build_pull_cache(tmpdir, docker_client, registry): 816 | path = str(tmpdir.join('shipwright-localhost-sample')) 817 | source = pkg_resources.resource_filename( 818 | __name__, 819 | 'examples/shipwright-localhost-sample', 820 | ) 821 | repo = create_repo(path, source) 822 | tag = repo.head.ref.commit.hexsha[:12] 823 | 824 | client_cfg = docker_utils.kwargs_from_env() 825 | cli = docker_client 826 | 827 | defaults = get_defaults() 828 | defaults['push'] = True 829 | try: 830 | shipw_cli.run( 831 | path=path, 832 | client_cfg=client_cfg, 833 | arguments=defaults, 834 | environ={}, 835 | ) 836 | 837 | # Remove the build images: 838 | old_images = ( 839 | cli.images(name='localhost:5000/service1', quiet=True) + 840 | cli.images(name='localhost:5000/shared', quiet=True) + 841 | cli.images(name='localhost:5000/base', quiet=True) 842 | ) 843 | for image in old_images: 844 | cli.remove_image(image, force=True) 845 | 846 | images_after_delete = ( 847 | cli.images(name='localhost:5000/service1') + 848 | cli.images(name='localhost:5000/shared') + 849 | cli.images(name='localhost:5000/base') 850 | ) 851 | assert images_after_delete == [] 852 | 853 | args = default_args() 854 | args.pull_cache = True 855 | 856 | shipw_cli.run( 857 | path=path, 858 | client_cfg=client_cfg, 859 | arguments=defaults, 860 | environ={}, 861 | new_style_args=args, 862 | ) 863 | 864 | service1, shared, base = ( 865 | cli.images(name='localhost:5000/service1') + 866 | cli.images(name='localhost:5000/shared') + 867 | cli.images(name='localhost:5000/base') 868 | ) 869 | 870 | assert set(service1['RepoTags']) == { 871 | 'localhost:5000/service1:master', 872 | 'localhost:5000/service1:latest', 873 | 'localhost:5000/service1:' + tag, 874 | } 875 | 876 | assert set(shared['RepoTags']) == { 877 | 'localhost:5000/shared:master', 878 | 'localhost:5000/shared:latest', 879 | 'localhost:5000/shared:' + tag, 880 | } 881 | 882 | assert set(base['RepoTags']) == { 883 | 'localhost:5000/base:master', 884 | 'localhost:5000/base:latest', 885 | 'localhost:5000/base:' + tag, 886 | } 887 | finally: 888 | old_images = ( 889 | cli.images(name='localhost:5000/service1', quiet=True) + 890 | cli.images(name='localhost:5000/shared', quiet=True) + 891 | cli.images(name='localhost:5000/base', quiet=True) 892 | ) 893 | for image in old_images: 894 | cli.remove_image(image, force=True) 895 | -------------------------------------------------------------------------------- /tests/integration/test_docker_push.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import pkg_resources 4 | from docker import utils as docker_utils 5 | 6 | from shipwright._lib import cli as shipw_cli 7 | 8 | from .utils import commit_untracked, create_repo, default_args, get_defaults 9 | 10 | 11 | def test_docker_push(tmpdir, docker_client, registry): 12 | path = str(tmpdir.join('shipwright-localhost-sample')) 13 | source = pkg_resources.resource_filename( 14 | __name__, 15 | 'examples/shipwright-localhost-sample', 16 | ) 17 | repo = create_repo(path, source) 18 | tag = repo.head.ref.commit.hexsha[:12] 19 | 20 | client_cfg = docker_utils.kwargs_from_env() 21 | cli = docker_client 22 | 23 | defaults = get_defaults() 24 | defaults['push'] = True 25 | try: 26 | shipw_cli.run( 27 | path=path, 28 | client_cfg=client_cfg, 29 | arguments=defaults, 30 | environ={}, 31 | ) 32 | 33 | # Remove the build images: 34 | old_images = ( 35 | cli.images(name='localhost:5000/service1', quiet=True) + 36 | cli.images(name='localhost:5000/shared', quiet=True) + 37 | cli.images(name='localhost:5000/base', quiet=True) 38 | ) 39 | for image in old_images: 40 | cli.remove_image(image, force=True) 41 | 42 | images_after_delete = ( 43 | cli.images(name='localhost:5000/service1') + 44 | cli.images(name='localhost:5000/shared') + 45 | cli.images(name='localhost:5000/base') 46 | ) 47 | assert images_after_delete == [] 48 | 49 | # Pull them again: 50 | for t in ['master', 'latest', tag]: 51 | for repo in ['service1', 'shared', 'base']: 52 | cli.pull('localhost:5000/' + repo, t) 53 | 54 | service1, shared, base = ( 55 | cli.images(name='localhost:5000/service1') + 56 | cli.images(name='localhost:5000/shared') + 57 | cli.images(name='localhost:5000/base') 58 | ) 59 | 60 | assert set(service1['RepoTags']) == { 61 | 'localhost:5000/service1:master', 62 | 'localhost:5000/service1:latest', 63 | 'localhost:5000/service1:' + tag, 64 | } 65 | 66 | assert set(shared['RepoTags']) == { 67 | 'localhost:5000/shared:master', 68 | 'localhost:5000/shared:latest', 69 | 'localhost:5000/shared:' + tag, 70 | } 71 | 72 | assert set(base['RepoTags']) == { 73 | 'localhost:5000/base:master', 74 | 'localhost:5000/base:latest', 75 | 'localhost:5000/base:' + tag, 76 | } 77 | finally: 78 | old_images = ( 79 | cli.images(name='localhost:5000/service1', quiet=True) + 80 | cli.images(name='localhost:5000/shared', quiet=True) + 81 | cli.images(name='localhost:5000/base', quiet=True) 82 | ) 83 | for image in old_images: 84 | cli.remove_image(image, force=True) 85 | 86 | 87 | def test_docker_no_build(tmpdir, docker_client, registry): 88 | path = str(tmpdir.join('shipwright-localhost-sample')) 89 | source = pkg_resources.resource_filename( 90 | __name__, 91 | 'examples/shipwright-localhost-sample', 92 | ) 93 | repo = create_repo(path, source) 94 | tag = repo.head.ref.commit.hexsha[:12] 95 | 96 | client_cfg = docker_utils.kwargs_from_env() 97 | cli = docker_client 98 | 99 | defaults = get_defaults() 100 | defaults.update({ 101 | 'push': True, 102 | '--no-build': True, 103 | }) 104 | try: 105 | # run a plain build so the images exist for push --no-build 106 | shipw_cli.run( 107 | path=path, 108 | client_cfg=client_cfg, 109 | arguments=get_defaults(), 110 | environ={}, 111 | ) 112 | shipw_cli.run( 113 | path=path, 114 | client_cfg=client_cfg, 115 | arguments=defaults, 116 | environ={}, 117 | ) 118 | 119 | # Remove the build images: 120 | old_images = ( 121 | cli.images(name='localhost:5000/service1', quiet=True) + 122 | cli.images(name='localhost:5000/shared', quiet=True) + 123 | cli.images(name='localhost:5000/base', quiet=True) 124 | ) 125 | for image in old_images: 126 | cli.remove_image(image, force=True) 127 | 128 | images_after_delete = ( 129 | cli.images(name='localhost:5000/service1') + 130 | cli.images(name='localhost:5000/shared') + 131 | cli.images(name='localhost:5000/base') 132 | ) 133 | assert images_after_delete == [] 134 | 135 | # Pull them again: 136 | for t in ['master', 'latest', tag]: 137 | for repo in ['service1', 'shared', 'base']: 138 | cli.pull('localhost:5000/' + repo, t) 139 | 140 | service1, shared, base = ( 141 | cli.images(name='localhost:5000/service1') + 142 | cli.images(name='localhost:5000/shared') + 143 | cli.images(name='localhost:5000/base') 144 | ) 145 | 146 | assert set(service1['RepoTags']) == { 147 | 'localhost:5000/service1:master', 148 | 'localhost:5000/service1:latest', 149 | 'localhost:5000/service1:' + tag, 150 | } 151 | 152 | assert set(shared['RepoTags']) == { 153 | 'localhost:5000/shared:master', 154 | 'localhost:5000/shared:latest', 155 | 'localhost:5000/shared:' + tag, 156 | } 157 | 158 | assert set(base['RepoTags']) == { 159 | 'localhost:5000/base:master', 160 | 'localhost:5000/base:latest', 161 | 'localhost:5000/base:' + tag, 162 | } 163 | finally: 164 | old_images = ( 165 | cli.images(name='localhost:5000/service1', quiet=True) + 166 | cli.images(name='localhost:5000/shared', quiet=True) + 167 | cli.images(name='localhost:5000/base', quiet=True) 168 | ) 169 | for image in old_images: 170 | cli.remove_image(image, force=True) 171 | 172 | 173 | def test_docker_push_target_ref(tmpdir, docker_client, registry): 174 | """ 175 | Test that shipwright push includes the target ref of every image. 176 | Otherwise --pull-cache will not work. 177 | """ 178 | tmp = tmpdir.join('shipwright-localhost-sample') 179 | path = str(tmp) 180 | source = pkg_resources.resource_filename( 181 | __name__, 182 | 'examples/shipwright-localhost-sample', 183 | ) 184 | repo = create_repo(path, source) 185 | tag = repo.head.ref.commit.hexsha[:12] 186 | 187 | tmp.join('service1/base.txt').write('Hi mum') 188 | commit_untracked(repo) 189 | new_tag = repo.head.ref.commit.hexsha[:12] 190 | 191 | client_cfg = docker_utils.kwargs_from_env() 192 | cli = docker_client 193 | 194 | defaults = get_defaults() 195 | defaults['push'] = True 196 | try: 197 | shipw_cli.run( 198 | path=path, 199 | client_cfg=client_cfg, 200 | arguments=defaults, 201 | environ={}, 202 | ) 203 | 204 | # Remove the build images: 205 | old_images = ( 206 | cli.images(name='localhost:5000/service1', quiet=True) + 207 | cli.images(name='localhost:5000/shared', quiet=True) + 208 | cli.images(name='localhost:5000/base', quiet=True) 209 | ) 210 | for image in old_images: 211 | cli.remove_image(image, force=True) 212 | 213 | images_after_delete = ( 214 | cli.images(name='localhost:5000/service1') + 215 | cli.images(name='localhost:5000/shared') + 216 | cli.images(name='localhost:5000/base') 217 | ) 218 | assert images_after_delete == [] 219 | 220 | # Pull them again: 221 | for t in ['master', 'latest', new_tag, tag]: 222 | for repo in ['service1', 'shared', 'base']: 223 | cli.pull('localhost:5000/' + repo, t) 224 | 225 | service1, shared, base = ( 226 | cli.images(name='localhost:5000/service1') + 227 | cli.images(name='localhost:5000/shared') + 228 | cli.images(name='localhost:5000/base') 229 | ) 230 | 231 | assert set(service1['RepoTags']) == { 232 | 'localhost:5000/service1:master', 233 | 'localhost:5000/service1:latest', 234 | 'localhost:5000/service1:' + new_tag, 235 | } 236 | 237 | assert set(shared['RepoTags']) == { 238 | 'localhost:5000/shared:master', 239 | 'localhost:5000/shared:latest', 240 | 'localhost:5000/shared:' + new_tag, 241 | 'localhost:5000/shared:' + tag, 242 | } 243 | 244 | assert set(base['RepoTags']) == { 245 | 'localhost:5000/base:master', 246 | 'localhost:5000/base:latest', 247 | 'localhost:5000/base:' + new_tag, 248 | 'localhost:5000/base:' + tag, 249 | } 250 | finally: 251 | old_images = ( 252 | cli.images(name='localhost:5000/service1', quiet=True) + 253 | cli.images(name='localhost:5000/shared', quiet=True) + 254 | cli.images(name='localhost:5000/base', quiet=True) 255 | ) 256 | for image in old_images: 257 | cli.remove_image(image, force=True) 258 | 259 | 260 | def test_docker_push_direct_registry(tmpdir, docker_client, registry): 261 | tmp = tmpdir.join('shipwright-localhost-sample') 262 | path = str(tmp) 263 | source = pkg_resources.resource_filename( 264 | __name__, 265 | 'examples/shipwright-localhost-sample', 266 | ) 267 | repo = create_repo(path, source) 268 | tag = repo.head.ref.commit.hexsha[:12] 269 | 270 | client_cfg = docker_utils.kwargs_from_env() 271 | cli = docker_client 272 | 273 | args = default_args() 274 | args.registry_login = [['docker login http://localhost:5000']] 275 | 276 | defaults = get_defaults() 277 | defaults['push'] = True 278 | try: 279 | 280 | shipw_cli.run( 281 | path=path, 282 | client_cfg=client_cfg, 283 | arguments=defaults, 284 | new_style_args=args, 285 | environ={}, 286 | ) 287 | 288 | # Remove the build images: 289 | old_images = ( 290 | cli.images(name='localhost:5000/service1', quiet=True) + 291 | cli.images(name='localhost:5000/shared', quiet=True) + 292 | cli.images(name='localhost:5000/base', quiet=True) 293 | ) 294 | for image in old_images: 295 | cli.remove_image(image, force=True) 296 | 297 | images_after_delete = ( 298 | cli.images(name='localhost:5000/service1') + 299 | cli.images(name='localhost:5000/shared') + 300 | cli.images(name='localhost:5000/base') 301 | ) 302 | assert images_after_delete == [] 303 | 304 | tmp.join('spam').write('Hi mum') 305 | commit_untracked(repo) 306 | new_tag = repo.head.ref.commit.hexsha[:12] 307 | 308 | shipw_cli.run( 309 | path=path, 310 | client_cfg=client_cfg, 311 | arguments=defaults, 312 | environ={}, 313 | new_style_args=args, 314 | ) 315 | 316 | images_after_build = ( 317 | cli.images(name='localhost:5000/service1') + 318 | cli.images(name='localhost:5000/shared') + 319 | cli.images(name='localhost:5000/base') 320 | ) 321 | 322 | # Nothing up my sleeve (Shipwright didn't download any images) 323 | assert images_after_build == [] 324 | 325 | # Pull them again: 326 | for t in ['master', 'latest', tag, new_tag]: 327 | for repo in ['service1', 'shared', 'base']: 328 | cli.pull('localhost:5000/' + repo, t) 329 | 330 | service1, shared, base = ( 331 | cli.images(name='localhost:5000/service1') + 332 | cli.images(name='localhost:5000/shared') + 333 | cli.images(name='localhost:5000/base') 334 | ) 335 | 336 | assert set(service1['RepoTags']) == { 337 | 'localhost:5000/service1:master', 338 | 'localhost:5000/service1:latest', 339 | 'localhost:5000/service1:' + tag, 340 | # But it still managed to tag a new commit! 341 | 'localhost:5000/service1:' + new_tag, 342 | } 343 | 344 | assert set(shared['RepoTags']) == { 345 | 'localhost:5000/shared:master', 346 | 'localhost:5000/shared:latest', 347 | 'localhost:5000/shared:' + tag, 348 | 'localhost:5000/shared:' + new_tag, 349 | } 350 | 351 | assert set(base['RepoTags']) == { 352 | 'localhost:5000/base:master', 353 | 'localhost:5000/base:latest', 354 | 'localhost:5000/base:' + tag, 355 | 'localhost:5000/base:' + new_tag, 356 | } 357 | finally: 358 | old_images = ( 359 | cli.images(name='localhost:5000/service1', quiet=True) + 360 | cli.images(name='localhost:5000/shared', quiet=True) + 361 | cli.images(name='localhost:5000/base', quiet=True) 362 | ) 363 | for image in old_images: 364 | cli.remove_image(image, force=True) 365 | -------------------------------------------------------------------------------- /tests/integration/test_git.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import pkg_resources 4 | import pytest 5 | 6 | from shipwright._lib import source_control 7 | 8 | from .utils import commit_untracked, create_repo 9 | 10 | 11 | def test_default_tags_works_with_detached_head(tmpdir): 12 | tmp = tmpdir.join('shipwright-sample') 13 | path = str(tmp) 14 | source = pkg_resources.resource_filename( 15 | __name__, 16 | 'examples/shipwright-sample', 17 | ) 18 | repo = create_repo(path, source) 19 | old_commit = repo.head.ref.commit 20 | tmp.join('service1/base.txt').write('Hi mum') 21 | commit_untracked(repo) 22 | 23 | repo.head.reference = old_commit 24 | 25 | scm = source_control.GitSourceControl( 26 | path=path, 27 | namespace=None, 28 | name_map=None, 29 | ) 30 | 31 | assert scm.default_tags() == [] 32 | 33 | 34 | def _refs(targets): 35 | return {target.image.name: target.ref for target in targets} 36 | 37 | 38 | def test_dirty_tags_untracked(tmpdir): 39 | tmp = tmpdir.join('shipwright-sample') 40 | path = str(tmp) 41 | source = pkg_resources.resource_filename( 42 | __name__, 43 | 'examples/shipwright-sample', 44 | ) 45 | repo = create_repo(path, source) 46 | 47 | scm = source_control.GitSourceControl( 48 | path=path, 49 | namespace='shipwright', 50 | name_map={}, 51 | ) 52 | assert not scm.is_dirty() 53 | 54 | old_refs = _refs(scm.targets()) 55 | old_ref_str = scm.this_ref_str() 56 | tag = repo.head.ref.commit.hexsha[:12] 57 | 58 | tmp.join('shared/base.txt').write('Hi mum') # Untracked 59 | assert scm.is_dirty() 60 | 61 | new_refs = _refs(scm.targets()) 62 | new_ref_str = scm.this_ref_str() 63 | 64 | assert new_refs['shipwright/base'] == old_refs['shipwright/base'] 65 | assert new_refs['shipwright/shared'] != old_refs['shipwright/shared'] 66 | assert new_refs['shipwright/service1'] != old_refs['shipwright/service1'] 67 | 68 | dirty_tag = tag + '-dirty-adc37b7d003f' 69 | assert new_refs == { 70 | 'shipwright/base': tag, 71 | 'shipwright/service1': dirty_tag, 72 | 'shipwright/shared': dirty_tag, 73 | } 74 | assert old_ref_str == tag 75 | assert new_ref_str == dirty_tag 76 | 77 | 78 | def test_dirty_tags_new_dir(tmpdir): 79 | tmp = tmpdir.join('shipwright-sample') 80 | path = str(tmp) 81 | source = pkg_resources.resource_filename( 82 | __name__, 83 | 'examples/shipwright-sample', 84 | ) 85 | repo = create_repo(path, source) 86 | 87 | scm = source_control.GitSourceControl( 88 | path=path, 89 | namespace='shipwright', 90 | name_map={}, 91 | ) 92 | assert not scm.is_dirty() 93 | 94 | old_ref_str = scm.this_ref_str() 95 | tag = repo.head.ref.commit.hexsha[:12] 96 | 97 | tmp.join('service2').mkdir() 98 | tmp.join('service2/Dockerfile').write('FROM busybox\n') 99 | assert scm.is_dirty() 100 | 101 | new_refs = _refs(scm.targets()) 102 | new_ref_str = scm.this_ref_str() 103 | 104 | dirty_suffix = '-dirty-f2f0df80ff0f' 105 | service2_tag = 'g' * 12 + dirty_suffix 106 | assert new_refs['shipwright/base'] == tag 107 | assert new_refs['shipwright/shared'] == tag 108 | assert new_refs['shipwright/service1'] == tag 109 | assert new_refs['shipwright/service2'] == service2_tag 110 | 111 | assert new_refs == { 112 | 'shipwright/base': tag, 113 | 'shipwright/service1': tag, 114 | 'shipwright/shared': tag, 115 | 'shipwright/service2': service2_tag, 116 | } 117 | assert old_ref_str == tag 118 | assert new_ref_str == tag + dirty_suffix 119 | 120 | 121 | def test_dirty_tags_tracked(tmpdir): 122 | tmp = tmpdir.join('shipwright-sample') 123 | path = str(tmp) 124 | source = pkg_resources.resource_filename( 125 | __name__, 126 | 'examples/shipwright-sample', 127 | ) 128 | repo = create_repo(path, source) 129 | 130 | scm = source_control.GitSourceControl( 131 | path=path, 132 | namespace='shipwright', 133 | name_map={}, 134 | ) 135 | assert not scm.is_dirty() 136 | 137 | old_refs = _refs(scm.targets()) 138 | old_ref_str = scm.this_ref_str() 139 | tag = repo.head.ref.commit.hexsha[:12] 140 | 141 | tmp.join('shared/base.txt').write('Hi mum') # Untracked 142 | repo.index.add(['shared/base.txt']) 143 | assert scm.is_dirty() 144 | 145 | new_refs = _refs(scm.targets()) 146 | new_ref_str = scm.this_ref_str() 147 | 148 | assert new_refs['shipwright/base'] == old_refs['shipwright/base'] 149 | assert new_refs['shipwright/shared'] != old_refs['shipwright/shared'] 150 | assert new_refs['shipwright/service1'] != old_refs['shipwright/service1'] 151 | 152 | dirty_tag = tag + '-dirty-adc37b7d003f' 153 | assert new_refs == { 154 | 'shipwright/base': tag, 155 | 'shipwright/service1': dirty_tag, 156 | 'shipwright/shared': dirty_tag, 157 | } 158 | assert old_ref_str == tag 159 | assert new_ref_str == dirty_tag 160 | 161 | 162 | def _git_modified_not_added_to_index(tmp, repo): 163 | tmp.join('base/base.txt').write('Hi again') # Modified, not added to index 164 | 165 | 166 | def _git_modified_version_added_to_index(tmp, repo): 167 | tmp.join('base/base.txt').write('Hi again') 168 | repo.index.add(['base/base.txt']) # Modified version added to index 169 | return '-dirty-5227fefb4c30' 170 | 171 | 172 | def _git_deleted_but_not_removed_from_index(tmp, repo): 173 | tmp.join('base/base.txt').write('Hi again') 174 | repo.index.add(['base/base.txt']) # Modified version added to index 175 | tmp.join('base/base.txt').remove() # Deleted, but not removed from index 176 | 177 | 178 | def _git_remove_from_index(tmp, repo): 179 | tmp.join('base/base.txt').write('Hi again') 180 | repo.index.add(['base/base.txt']) # Modified version added to index 181 | tmp.join('base/base.txt').remove() # Deleted, but not removed from index 182 | repo.index.remove(['base/base.txt']) # Remove from index 183 | 184 | 185 | _various_git_functions = [ 186 | _git_modified_not_added_to_index, 187 | _git_modified_version_added_to_index, 188 | _git_deleted_but_not_removed_from_index, 189 | _git_remove_from_index, 190 | ] 191 | 192 | 193 | @pytest.mark.parametrize('func', _various_git_functions) 194 | def test_dirty_tags_various(func, tmpdir): 195 | tmp = tmpdir.join('shipwright-sample') 196 | path = str(tmp) 197 | source = pkg_resources.resource_filename( 198 | __name__, 199 | 'examples/shipwright-sample', 200 | ) 201 | repo = create_repo(path, source) 202 | 203 | scm = source_control.GitSourceControl( 204 | path=path, 205 | namespace='shipwright', 206 | name_map={}, 207 | ) 208 | assert not scm.is_dirty() 209 | 210 | old_refs = _refs(scm.targets()) 211 | old_ref_str = scm.this_ref_str() 212 | tag = repo.head.ref.commit.hexsha[:12] 213 | 214 | expected_tag_suffix = func(tmp, repo) or '-dirty-0d6d6d2f8739' 215 | assert scm.is_dirty() 216 | 217 | new_refs = _refs(scm.targets()) 218 | new_ref_str = scm.this_ref_str() 219 | 220 | assert new_refs['shipwright/base'] != old_refs['shipwright/base'] 221 | dirty_tag = tag + expected_tag_suffix 222 | assert new_refs == { 223 | 'shipwright/base': dirty_tag, 224 | 'shipwright/service1': dirty_tag, 225 | 'shipwright/shared': dirty_tag, 226 | } 227 | assert old_ref_str == tag 228 | assert new_ref_str == dirty_tag 229 | -------------------------------------------------------------------------------- /tests/integration/test_images.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import argparse 4 | 5 | import pkg_resources 6 | from docker import utils as docker_utils 7 | 8 | from shipwright._lib import cli as shipw_cli 9 | 10 | from .utils import create_repo, get_defaults 11 | 12 | 13 | def default_args(): 14 | return argparse.Namespace(dirty=False, pull_cache=False) 15 | 16 | 17 | def test_sample(tmpdir, capsys): 18 | path = str(tmpdir.join('shipwright-sample')) 19 | source = pkg_resources.resource_filename( 20 | __name__, 21 | 'examples/shipwright-sample', 22 | ) 23 | repo = create_repo(path, source) 24 | tag = repo.head.ref.commit.hexsha[:12] 25 | 26 | client_cfg = docker_utils.kwargs_from_env() 27 | args = get_defaults() 28 | args['images'] = True 29 | 30 | shipw_cli.run( 31 | path=path, 32 | client_cfg=client_cfg, 33 | arguments=args, 34 | environ={}, 35 | ) 36 | 37 | out, err = capsys.readouterr() 38 | images = {'base', 'shared', 'service1'} 39 | tmpl = 'shipwright/{img}:{tag}' 40 | expected = {tmpl.format(img=i, tag=tag) for i in images} 41 | 42 | assert {l for l in out.split('\n') if l} == expected 43 | -------------------------------------------------------------------------------- /tests/integration/test_targets.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import pkg_resources 4 | import pytest 5 | 6 | from shipwright import exceptions, targets 7 | 8 | from . import utils 9 | 10 | 11 | def test_simple(tmpdir): 12 | tmp = tmpdir.join('shipwright-sample') 13 | path = str(tmp) 14 | source = pkg_resources.resource_filename( 15 | __name__, 16 | 'examples/shipwright-sample', 17 | ) 18 | repo = utils.create_repo(path, source) 19 | commit = repo.head.ref.commit.hexsha[:12] 20 | assert targets.targets(path=path) == [ 21 | 'shipwright/base:' + commit, 22 | 'shipwright/base:master', 23 | 'shipwright/base:' + commit, 24 | 'shipwright/shared:' + commit, 25 | 'shipwright/shared:master', 26 | 'shipwright/shared:' + commit, 27 | 'shipwright/service1:' + commit, 28 | 'shipwright/service1:master', 29 | 'shipwright/service1:' + commit, 30 | ] 31 | 32 | 33 | def test_upto(tmpdir): 34 | tmp = tmpdir.join('shipwright-sample') 35 | path = str(tmp) 36 | source = pkg_resources.resource_filename( 37 | __name__, 38 | 'examples/shipwright-sample', 39 | ) 40 | repo = utils.create_repo(path, source) 41 | commit = repo.head.ref.commit.hexsha[:12] 42 | assert targets.targets(path=path, upto=['shared']) == [ 43 | 'shipwright/base:' + commit, 44 | 'shipwright/base:master', 45 | 'shipwright/base:' + commit, 46 | 'shipwright/shared:' + commit, 47 | 'shipwright/shared:master', 48 | 'shipwright/shared:' + commit, 49 | ] 50 | 51 | 52 | def test_extra_tags(tmpdir): 53 | tmp = tmpdir.join('shipwright-sample') 54 | path = str(tmp) 55 | source = pkg_resources.resource_filename( 56 | __name__, 57 | 'examples/shipwright-sample', 58 | ) 59 | repo = utils.create_repo(path, source) 60 | commit = repo.head.ref.commit.hexsha[:12] 61 | assert targets.targets(path=path, upto=['shared'], tags=['ham/spam']) == [ 62 | 'shipwright/base:' + commit, 63 | 'shipwright/base:ham-spam', 64 | 'shipwright/base:master', 65 | 'shipwright/base:' + commit, 66 | 'shipwright/shared:' + commit, 67 | 'shipwright/shared:ham-spam', 68 | 'shipwright/shared:master', 69 | 'shipwright/shared:' + commit, 70 | ] 71 | 72 | 73 | def test_no_repo(tmpdir): 74 | tmp = tmpdir.join('shipwright-sample') 75 | path = str(tmp) 76 | source = pkg_resources.resource_filename( 77 | __name__, 78 | 'examples/shipwright-sample', 79 | ) 80 | utils.create_repo(path, source) 81 | tmp.join('.git').remove(rec=1) 82 | with pytest.raises(exceptions.SourceControlNotFound): 83 | targets.targets(path=path) 84 | -------------------------------------------------------------------------------- /tests/integration/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import argparse 4 | import shutil 5 | 6 | import git 7 | 8 | 9 | def get_defaults(): 10 | return { 11 | '--account': None, 12 | '--dependents': [], 13 | '--dump-file': None, 14 | '--exact': [], 15 | '--exclude': [], 16 | '--help': False, 17 | '--no-build': False, 18 | '--dirty': False, 19 | '--upto': [], 20 | '--x-assert-hostname': False, 21 | '-H': None, 22 | 'TARGET': [], 23 | 'build': False, 24 | 'push': False, 25 | 'images': False, 26 | 'tags': ['latest'], 27 | } 28 | 29 | 30 | def create_repo(path, source=None): 31 | if source is not None: 32 | shutil.copytree(source, path) 33 | repo = git.Repo.init(path) 34 | repo.index.add(repo.untracked_files) 35 | repo.index.commit('Initial Commit') 36 | return repo 37 | 38 | 39 | def commit_untracked(repo, message='WIP'): 40 | repo.index.add(repo.untracked_files) 41 | repo.index.commit(message) 42 | 43 | 44 | def default_args(): 45 | return argparse.Namespace( 46 | dirty=False, 47 | pull_cache=False, 48 | registry_login=[], 49 | ) 50 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import git 4 | import pytest 5 | from docker import tls 6 | 7 | from shipwright._lib import cli 8 | 9 | 10 | def get_defaults(): 11 | return { 12 | '--account': None, 13 | '--dependents': [], 14 | '--dump-file': None, 15 | '--exact': [], 16 | '--exclude': [], 17 | '--help': False, 18 | '--no-build': False, 19 | '--upto': [], 20 | '--x-assert-hostname': False, 21 | '-H': None, 22 | 'TARGET': [], 23 | 'build': False, 24 | 'push': False, 25 | 'images': False, 26 | 'tags': ['latest'], 27 | } 28 | 29 | 30 | def create_repo(path): 31 | repo = git.Repo.init(path) 32 | repo.index.add(repo.untracked_files) 33 | repo.index.commit('Initial Commit') 34 | return repo 35 | 36 | 37 | def test_without_json_manifest(tmpdir): 38 | path = str(tmpdir.join('no-manifest')) 39 | create_repo(path) 40 | with pytest.raises(SystemExit): 41 | cli.process_arguments( 42 | path, get_defaults(), client_cfg={}, environ={}, 43 | ) 44 | 45 | 46 | def test_push_also_builds(tmpdir): 47 | path = str(tmpdir.join('no-manifest')) 48 | create_repo(path) 49 | in_args = get_defaults() 50 | in_args['push'] = True 51 | _, no_build, _, _, _, _ = cli.process_arguments( 52 | path, in_args, client_cfg={}, 53 | environ={'SW_NAMESPACE': 'eg'}, 54 | ) 55 | assert not no_build 56 | 57 | 58 | def test_assert_hostname(tmpdir): 59 | path = str(tmpdir.join('no-manifest')) 60 | create_repo(path) 61 | args = get_defaults() 62 | args['--x-assert-hostname'] = True 63 | tls_config = tls.TLSConfig() 64 | _, _, _, _, _, client = cli.process_arguments( 65 | path, args, 66 | client_cfg={ 67 | 'base_url': 'https://example.com:443/api/v1/', 68 | 'tls': tls_config, 69 | }, 70 | environ={'SW_NAMESPACE': 'eg'}, 71 | ) 72 | 73 | assert not client.adapters['https://'].assert_hostname 74 | 75 | 76 | def test_args(): 77 | args = [ 78 | '--account=x', '--x-assert-hostname', 'build', 79 | '-d', 'foo', '-d', 'bar', 80 | '-t', 'latest', '-t', 'foo', 81 | ] 82 | parser = cli.argparser() 83 | arguments = cli.old_style_arg_dict(parser.parse_args(args)) 84 | 85 | assert arguments == { 86 | '--account': 'x', 87 | '--dependents': ['foo', 'bar'], 88 | '--dump-file': None, 89 | '--exact': [], 90 | '--exclude': [], 91 | '--help': False, 92 | '--no-build': False, 93 | '--upto': [], 94 | '--x-assert-hostname': True, 95 | '-H': None, 96 | 'TARGET': [], 97 | 'build': True, 98 | 'push': False, 99 | 'images': False, 100 | 'tags': ['foo', 'latest'], 101 | } 102 | 103 | 104 | def test_args_2(): 105 | args = [ 106 | '--account=x', '--x-assert-hostname', 'build', 107 | '-d', 'foo', 'bar', 108 | '-t', 'foo', '--dirty', '--pull-cache', 109 | ] 110 | parser = cli.argparser() 111 | arguments = cli.old_style_arg_dict(parser.parse_args(args)) 112 | 113 | assert arguments == { 114 | '--account': 'x', 115 | '--dependents': ['foo', 'bar'], 116 | '--dump-file': None, 117 | '--exact': [], 118 | '--exclude': [], 119 | '--help': False, 120 | '--no-build': False, 121 | '--upto': [], 122 | '--x-assert-hostname': True, 123 | '-H': None, 124 | 'TARGET': [], 125 | 'build': True, 126 | 'push': False, 127 | 'images': False, 128 | 'tags': ['foo'], 129 | } 130 | 131 | 132 | def test_args_base(): 133 | args = ['build'] 134 | parser = cli.argparser() 135 | arguments = cli.old_style_arg_dict(parser.parse_args(args)) 136 | 137 | assert arguments == { 138 | '--account': None, 139 | '--dependents': [], 140 | '--dump-file': None, 141 | '--exact': [], 142 | '--exclude': [], 143 | '--help': False, 144 | '--no-build': False, 145 | '--upto': [], 146 | '--x-assert-hostname': False, 147 | '-H': None, 148 | 'TARGET': [], 149 | 'build': True, 150 | 'push': False, 151 | 'images': False, 152 | 'tags': ['latest'], 153 | } 154 | -------------------------------------------------------------------------------- /tests/test_dependencies.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from shipwright._lib import dependencies, image, source_control 4 | 5 | 6 | def names_list(targets): 7 | return sorted(n.name for n in targets) 8 | 9 | 10 | def _names(tree): 11 | return [n.name for n in dependencies._brood(tree)] 12 | 13 | 14 | def target(name, dir_path, path, parent): 15 | return source_control.Target( 16 | image.Image(name, dir_path, path, parent, name, frozenset([path])), 17 | 'abc', None, 18 | ) 19 | 20 | 21 | targets = [ 22 | target( 23 | 'shipwright_test/2', 'path2/', 'path2/Dockerfile', 24 | 'shipwright_test/1', 25 | ), 26 | target( 27 | 'shipwright_test/1', 'path1/', 'path1/Dockerfile', 28 | 'ubuntu', 29 | ), 30 | target( 31 | 'shipwright_test/3', 'path3/', 'path3/Dockerfile', 32 | 'shipwright_test/2', 33 | ), 34 | target( 35 | 'shipwright_test/independent', 'independent', 36 | 'path1/Dockerfile', 'ubuntu', 37 | ), 38 | ] 39 | 40 | 41 | def default_build_targets(): 42 | return { 43 | 'exact': [], 44 | 'dependents': [], 45 | 'upto': [], 46 | 'exclude': [], 47 | } 48 | 49 | 50 | def test_upto(): 51 | bt = default_build_targets() 52 | bt['upto'] = ['shipwright_test/2'] 53 | result = names_list(dependencies.eval(bt, targets)) 54 | assert result == ['shipwright_test/1', 'shipwright_test/2'] 55 | 56 | 57 | def test_dependents(): 58 | bt = default_build_targets() 59 | bt['dependents'] = ['shipwright_test/2'] 60 | result = names_list(dependencies.eval(bt, targets)) 61 | assert result == [ 62 | 'shipwright_test/1', 'shipwright_test/2', 'shipwright_test/3', 63 | ] 64 | 65 | 66 | def test_exact(): 67 | bt = default_build_targets() 68 | bt['exact'] = ['shipwright_test/2'] 69 | result = names_list(dependencies.eval(bt, targets)) 70 | assert result == ['shipwright_test/2'] 71 | 72 | 73 | def test_exclude(): 74 | bt = default_build_targets() 75 | bt['exclude'] = ['shipwright_test/2', 'fake_exclude'] 76 | result = names_list(dependencies.eval(bt, targets)) 77 | assert result == ['shipwright_test/1', 'shipwright_test/independent'] 78 | 79 | 80 | def test_breadth_first_iter(): 81 | bt = default_build_targets() 82 | results = [result.name for result in dependencies.eval(bt, targets)] 83 | assert results == [ 84 | 'shipwright_test/1', 85 | 'shipwright_test/independent', 86 | 'shipwright_test/2', 87 | 'shipwright_test/3', 88 | ] 89 | 90 | 91 | def test_make_tree(): 92 | root = dependencies._make_tree(targets) 93 | assert root.node().name is None 94 | 95 | assert _names(root) == [ 96 | 'shipwright_test/1', 97 | 'shipwright_test/independent', 98 | 'shipwright_test/2', 99 | 'shipwright_test/3', 100 | ] 101 | 102 | sr_test_1 = root.down().node() 103 | assert sr_test_1.image.name == 'shipwright_test/1' 104 | 105 | assert _names(root.down()) == ['shipwright_test/2', 'shipwright_test/3'] 106 | 107 | sr_test_2 = root.down().down().node() 108 | assert sr_test_2.image.name == 'shipwright_test/2' 109 | 110 | assert _names(root.down().down()) == ['shipwright_test/3'] 111 | assert root.down().right().node().name == 'shipwright_test/independent' 112 | -------------------------------------------------------------------------------- /tests/test_switch.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from shipwright._lib import cli 4 | 5 | 6 | def switch(event): 7 | return cli.switch(event, True) 8 | 9 | 10 | aux = { 11 | u'aux': { 12 | u'Digest': u'sha256:redacted', 13 | u'Size': 1337, 14 | u'Tag': u'redacted', 15 | }, 16 | 'event': 'push', 17 | 'image': u'redacted/redacted', 18 | u'progressDetail': {}, 19 | } 20 | 21 | unhandled = { 22 | 'event': 'unhandled', 23 | } 24 | 25 | 26 | def test_aux_record(): 27 | assert switch(aux) is None 28 | 29 | 30 | def test_unhandled_record(): 31 | assert switch(unhandled) == '{"event": "unhandled"}' 32 | 33 | 34 | def test_status(): 35 | assert switch({ 36 | 'status': 'Not Downloading xyz', 37 | 'id': 'eg', 38 | }) == '[STATUS] eg: Not Downloading xyz' 39 | 40 | 41 | def test_progress(): 42 | evt = { 43 | 'status': 'Downloading xyz', 44 | 'id': 'eg', 45 | 'progressDetail': {'current': 10, 'total': 100}, 46 | } 47 | assert cli.switch(evt, True) == '[STATUS] eg: Downloading xyz 10/100\r' 48 | 49 | 50 | def test_hide_progress(): 51 | evt = { 52 | 'status': 'Downloading xyz', 53 | 'id': 'eg', 54 | 'progressDetail': {'current': 10, 'total': 100}, 55 | } 56 | assert cli.switch(evt, False) is None 57 | 58 | 59 | def test_error(): 60 | assert switch({ 61 | 'error': None, 62 | 'errorDetail': { 63 | 'message': 'I AM ERROR', 64 | }, 65 | }) == '[ERROR] I AM ERROR' 66 | -------------------------------------------------------------------------------- /tests/test_tar.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import tarfile 4 | 5 | from shipwright._lib import tar 6 | 7 | 8 | def test_mkcontext(tmpdir): 9 | tmp = tmpdir.mkdir('image') 10 | docker_path = tmp.join('Dockerfile') 11 | docker_path.write('FROM example.com/r/image') 12 | tmp.join('bogus').write('hi mom') 13 | 14 | with tar.mkcontext('xyz', str(docker_path)) as f: 15 | with tarfile.open(fileobj=f, mode='a') as t: 16 | names = t.getnames() 17 | 18 | assert names == ['', 'bogus', 'Dockerfile'] 19 | 20 | 21 | def test_mkcontext_dockerignore(tmpdir): 22 | tmp = tmpdir.mkdir('image') 23 | docker_path = tmp.join('Dockerfile') 24 | docker_path.write('FROM example.com/r/image') 25 | tmp.join('.dockerignore').write('bogus2') 26 | tmp.join('bogus').write('hi mom') 27 | tmp.join('bogus2').write('This is ignored') 28 | 29 | with tar.mkcontext('xyz', str(docker_path)) as f: 30 | with tarfile.open(fileobj=f, mode='a') as t: 31 | names = t.getnames() 32 | 33 | assert names == ['', '.dockerignore', 'bogus', 'Dockerfile'] 34 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{27,34,35}, lint, isort 3 | 4 | [testenv] 5 | commands = 6 | py.test {posargs} 7 | deps = 8 | docker-registry-client==0.5.1 9 | pytest==3.1.1 10 | pytest-cov==2.5.1 11 | 12 | [testenv:lint] 13 | deps = 14 | flake8==3.3.0 15 | flake8-commas==0.4.3 16 | flake8-quotes==0.11.0 17 | commands=flake8 shipwright tests setup.py 18 | 19 | [testenv:isort] 20 | deps = 21 |     isort==4.2.14 22 | pytest==3.1.1 23 | pytest-cov==2.5.1 24 | commands=isort --check-only --diff --recursive shipwright tests setup.py 25 | 26 | [pytest] 27 | addopts = --cov=shipwright --doctest-modules --doctest-glob='*.rst' --ignore=setup.py 28 | 29 | [flake8] 30 | ignore = C815 31 | --------------------------------------------------------------------------------