├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CONTRIBUTING.md ├── Dockerfile.build ├── ISSUE_TEMPLATE.md ├── LICENSE ├── Makefile ├── README.md ├── bash_completion ├── dvm-helper ├── build ├── checksum │ └── checksum.go ├── dockerversion │ ├── dockerversion.go │ ├── dockerversion.nix.go │ ├── dockerversion_386.go │ ├── dockerversion_amd64.go │ ├── dockerversion_arm64.go │ ├── dockerversion_darwin.go │ ├── dockerversion_linux.go │ ├── dockerversion_test.go │ ├── dockerversion_windows.go │ ├── edge_version.go │ ├── edge_version_test.go │ ├── query.go │ └── testdata │ │ └── edge_releases.html ├── dvm-helper.386.go ├── dvm-helper.amd64.go ├── dvm-helper.arm64.go ├── dvm-helper.darwin.go ├── dvm-helper.go ├── dvm-helper.linux.go ├── dvm-helper.nix.go ├── dvm-helper.windows.go ├── dvm-helper_test.go ├── internal │ ├── config │ │ └── config.go │ ├── downloader │ │ └── downloader.go │ └── test │ │ └── testdata.go ├── path.go ├── testdata │ └── github-docker-releases.json ├── url │ └── url.go └── util.go ├── dvm.cmd ├── dvm.ps1 ├── dvm.sh ├── go.mod ├── go.sum ├── install.ps1 ├── install.sh └── script ├── deploy-gh-pages.sh └── travis.deploy.pem.enc /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - name: Install Go 11 | uses: actions/setup-go@v3 12 | with: 13 | go-version: 1.18 14 | - name: Build 15 | run: go build -v ./... 16 | - name: Test 17 | run: go test ./... 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | _deploy 10 | 11 | # Architecture specific extensions/prefixes 12 | *.[568vq] 13 | [568vq].out 14 | 15 | *.cgo1.go 16 | *.cgo2.c 17 | _cgo_defun.c 18 | _cgo_gotypes.go 19 | _cgo_export.* 20 | 21 | _testmain.go 22 | 23 | *.exe 24 | *.test 25 | *.prof 26 | 27 | # Temporary files 28 | .tmp 29 | .DS_Store 30 | 31 | # Downloads 32 | bin 33 | dvm-helper/dvm-helper 34 | 35 | # Aliases 36 | alias 37 | *.pem 38 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 1. Clone this repository 4 | 5 | ``` 6 | go get -d github.com/howtowhale/dvm 7 | cd $GOPATH/src/github.com/howtowhale/dvm 8 | ``` 9 | 10 | 1. Add your fork as a remote: 11 | 12 | ``` 13 | git remote add fork git@github.com:USERNAME/dvm.git 14 | ``` 15 | 16 | 1. Build `dvm`: 17 | 18 | ``` 19 | make 20 | ``` 21 | 22 | 1. Run tests: 23 | 24 | ``` 25 | make test 26 | ``` 27 | 28 | 1. Try out your local build: 29 | 30 | ``` 31 | source ./dvm.sh 32 | dvm --version 33 | ``` 34 | -------------------------------------------------------------------------------- /Dockerfile.build: -------------------------------------------------------------------------------- 1 | FROM golang:1.18 2 | 3 | ADD . $GOPATH/src/github.com/howtowhale/dvm/ 4 | WORKDIR $GOPATH/src/github.com/howtowhale/dvm/ 5 | 6 | RUN make get-deps 7 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | If you are reporting a bug, please use the template below: 2 | 3 | ### What didn't work? 4 | Run the command(s) that failed (or did not behave properly) again with the `--debug` flag and include the output. 5 | 6 | ``` 7 | $ dvm --debug [REPLACE WITH YOUR COMMANDS] 8 | [PASTE YOUR OUTPUT HERE] 9 | ``` 10 | 11 | ### What should it have done? 12 | Let us know what you expected the command above to do. 13 | 14 | ### Helpful Context 15 | If you can provide the following info, it will help us figure things out more quickly! :smile: 16 | 17 | What version of dvm are you using? 18 | 19 | ``` 20 | $ dvm --version 21 | [PASTE YOUR OUTPUT HERE] 22 | ``` 23 | 24 | What is your Operating System? 25 | 26 | What is your shell (bash/powershell/cmd/fish)? 27 | 28 | How did you install dvm (command-line,chocolatey,homebrew)? 29 | 30 | Do you like kittens? :smile_cat: 31 | -------------------------------------------------------------------------------- /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 | 203 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | 3 | COMMIT = $(shell git rev-parse --verify --short HEAD) 4 | VERSION = $(shell git describe --tags --dirty='-dev' 2> /dev/null) 5 | BREW_VERSION = $(shell git describe --tags --abbrev=0 2> /dev/null) 6 | PERMALINK = $(shell if [[ $(VERSION) =~ [^-]*-([^.]+).* ]]; then echo $${BASH_REMATCH[1]}; else echo "latest"; fi) 7 | 8 | GITHUB_ORG = howtowhale 9 | GITHUB_REPO = dvm 10 | PACKAGE = github.com/${GITHUB_ORG}/${GITHUB_REPO}/dvm-helper 11 | UPGRADE_DISABLED = false 12 | 13 | LDFLAGS = -w -X main.dvmCommit=${COMMIT} -X main.dvmVersion=${VERSION} -X main.upgradeDisabled=${UPGRADE_DISABLED} 14 | 15 | GOCMD = go 16 | GOBUILD = $(GOCMD) build -a -tags netgo -ldflags '$(LDFLAGS)' 17 | 18 | BINDIR = bin/dvm/$(VERSION) 19 | GOFILES = dvm-helper/*.go 20 | GOFILES_NOVENDOR = $(shell go list ./... | grep -v /vendor/) 21 | 22 | default: local 23 | 24 | homebrew: 25 | brew bump-formula-pr --strict --url=https://github.com/howtowhale/dvm/archive/$(BREW_VERSION).tar.gz dvm 26 | 27 | validate: 28 | go fmt $(GOFILES_NOVENDOR) 29 | go vet $(GOFILES_NOVENDOR) 30 | -go list ./... | grep -v /vendor/ | xargs -L1 golint --set_exit_status 31 | 32 | test: local 33 | go test -v $(GOFILES_NOVENDOR) 34 | eval "$( ./dvm-helper --bash-completion )" 35 | ./dvm-helper/dvm-helper --version 36 | 37 | cross-build: local linux linux32 darwin windows windows32 38 | cp dvm.sh dvm.ps1 dvm.cmd install.sh install.ps1 README.md LICENSE bash_completion $(BINDIR)/ 39 | find $(BINDIR) -maxdepth 1 -name "install.*" -exec sed -i -e 's/$(PERMALINK)/$(VERSION)/g' {} \; 40 | cp -R $(BINDIR) bin/dvm/$(PERMALINK) 41 | 42 | local: $(GOFILES) 43 | CGO_ENABLED=0 $(GOBUILD) -o dvm-helper/dvm-helper $(PACKAGE) 44 | 45 | linux: $(GOFILES) 46 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) -o $(BINDIR)/Linux/x86_64/dvm-helper $(PACKAGE) 47 | cd $(BINDIR)/Linux/x86_64 && shasum -a 256 dvm-helper > dvm-helper.sha256 48 | 49 | linux32: $(GOFILES) 50 | CGO_ENABLED=0 GOOS=linux GOARCH=386 $(GOBUILD) -o $(BINDIR)/Linux/i686/dvm-helper $(PACKAGE) 51 | cd $(BINDIR)/Linux/i686 && shasum -a 256 dvm-helper > dvm-helper.sha256 52 | 53 | darwin: $(GOFILES) 54 | CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(GOBUILD) -o $(BINDIR)/Darwin/x86_64/dvm-helper $(PACKAGE) 55 | cd $(BINDIR)/Darwin/x86_64 && shasum -a 256 dvm-helper > dvm-helper.sha256 56 | 57 | windows: $(GOFILES) 58 | CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GOBUILD) -o $(BINDIR)/Windows/x86_64/dvm-helper.exe $(PACKAGE) 59 | cd $(BINDIR)/Windows/x86_64 && shasum -a 256 dvm-helper.exe > dvm-helper.exe.sha256 60 | 61 | windows32: $(GOFILES) 62 | CGO_ENABLED=0 GOOS=windows GOARCH=386 $(GOBUILD) -o $(BINDIR)/Windows/i686/dvm-helper.exe $(PACKAGE) 63 | cd $(BINDIR)/Windows/i686 && shasum -a 256 dvm-helper.exe > dvm-helper.exe.sha256 64 | 65 | # To make a release, push a tag to master, e.g. git tag 0.2.0 -a -m "" 66 | 67 | .PHONY: clean deploy 68 | 69 | clean: 70 | -rm -fr bin/* 71 | -rm -fr _deploy/ 72 | -rm dvm-helper/dvm-helper 73 | -rm dvm-helper/dvm-helper.exe 74 | 75 | deploy: 76 | VERSION=$(VERSION) PERMALINK=$(PERMALINK) ./script/deploy-gh-pages.sh 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docker Version Manager 2 | 3 |

howtowhale.github.io/dvm/

4 | 5 | ![dvm-usage](https://cloud.githubusercontent.com/assets/1368985/10800443/d3f0f39a-7d7f-11e5-87b5-1bda5ffe4859.png) 6 | 7 | Version management for your Docker client. Escape from this error for a little bit longer: 8 | 9 | ``` 10 | Error response from daemon: client and server don't have same version (client : 1.18, server: 1.16) 11 | ``` 12 | 13 | # Contributing 14 | See our [Contributing Guide](CONTRIBUTING.md). 15 | -------------------------------------------------------------------------------- /bash_completion: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # bash completion for Docker Version Manager (DVM) 4 | 5 | __dvm_generate_completion() 6 | { 7 | declare current_word 8 | current_word="${COMP_WORDS[COMP_CWORD]}" 9 | COMPREPLY=($(compgen -W "$1" -- "$current_word")) 10 | return 0 11 | } 12 | 13 | __dvm_commands () 14 | { 15 | declare current_word 16 | declare command 17 | 18 | current_word="${COMP_WORDS[COMP_CWORD]}" 19 | 20 | COMMANDS='\ 21 | help install uninstall use \ 22 | alias unalias upgrade \ 23 | current list ls list-remote ls-remote \ 24 | list-alias ls-alias deactivate unload \ 25 | version which' 26 | 27 | if [ ${#COMP_WORDS[@]} == 4 ]; then 28 | 29 | command="${COMP_WORDS[COMP_CWORD-2]}" 30 | case "${command}" in 31 | alias) __dvm_installed_dockers ;; 32 | esac 33 | 34 | else 35 | 36 | case "${current_word}" in 37 | -*) __dvm_options ;; 38 | *) __dvm_generate_completion "$COMMANDS" ;; 39 | esac 40 | 41 | fi 42 | } 43 | 44 | __dvm_options () 45 | { 46 | OPTIONS='' 47 | __dvm_generate_completion "$OPTIONS" 48 | } 49 | 50 | __dvm_installed_dockers () 51 | { 52 | __dvm_generate_completion "$(__dvm_ls) $(__dvm_aliases)" 53 | } 54 | 55 | __dvm_ls () 56 | { 57 | declare installed 58 | installed="" 59 | if [ -d $DVM_DIR/bin/docker ]; then 60 | installed="`cd $DVM_DIR/bin/docker && command ls`" 61 | fi 62 | echo "${installed}" 63 | } 64 | 65 | __dvm_aliases () 66 | { 67 | declare aliases 68 | aliases="" 69 | if [ -d $DVM_DIR/alias ]; then 70 | aliases="`cd $DVM_DIR/alias && command ls`" 71 | fi 72 | echo "${aliases}" 73 | } 74 | 75 | __dvm_alias () 76 | { 77 | __dvm_generate_completion "$(__dvm_aliases)" 78 | } 79 | 80 | __dvm () 81 | { 82 | declare previous_word 83 | previous_word="${COMP_WORDS[COMP_CWORD-1]}" 84 | 85 | case "$previous_word" in 86 | use|ls|list|uninstall) __dvm_installed_dockers ;; 87 | alias|unalias) __dvm_alias ;; 88 | *) __dvm_commands ;; 89 | esac 90 | 91 | return 0 92 | } 93 | 94 | # complete is a bash builtin, but recent versions of ZSH come with a function 95 | # called bashcompinit that will create a complete in ZSH. If the user is in 96 | # ZSH, load and run bashcompinit before calling the complete function. 97 | if [[ -n ${ZSH_VERSION-} ]]; then 98 | autoload -U +X bashcompinit && bashcompinit 99 | fi 100 | 101 | complete -o default -o nospace -F __dvm dvm 102 | -------------------------------------------------------------------------------- /dvm-helper/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # build - builds for the current OS and ARCH 3 | # build all - builds entire matrix 4 | 5 | cd $( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 6 | 7 | get_commit() { 8 | COMMIT=$(git rev-parse --verify --short HEAD) 9 | RETURN_CODE=$? 10 | export COMMIT 11 | return $RETURN_CODE 12 | } 13 | 14 | get_version() { 15 | # Version will be the most recent tag + "-dev" if working tree is dirty 16 | # e.g.: "1.0.1"" or "1.0.1-dev" 17 | VERSION=$(git describe --tags --dirty='-dev' 2> /dev/null) 18 | export VERSION 19 | return 0 20 | } 21 | 22 | build() { 23 | # Detect the current OS and ARCH if not specified 24 | [ -z "$1" ] && local GOOS=$(uname -s | tr '[:upper:]' '[:lower:]') || local GOOS=$1 25 | [ -z "$2" ] && local GOARCH=$([ $(getconf LONG_BIT) == 64 ] && echo amd64 || echo 386) || local GOARCH=$2 26 | [ "$GOOS" == "windows" ] && local FILE_EXT=".exe" || local FILE_EXT="" 27 | local BINARY="dvm-helper-${GOOS}-${GOARCH}${FILE_EXT}" 28 | 29 | GOOS=$GOOS GOARCH=$GOARCH go build -ldflags "${LDFLAGS}" -o bin/$BINARY 30 | $(cd bin && shasum -a 256 $BINARY > $BINARY.sha256) 31 | } 32 | 33 | # Remove previously downloaded dvm-helper binaries 34 | if [ -e 'dvm-helper' ]; then 35 | rm dvm-helper 36 | fi 37 | if [ -e 'dvm-helper.exe' ]; then 38 | rm dvm-helper.exe 39 | fi 40 | 41 | # Use information about git repo to set the binary version and commit 42 | get_commit 43 | get_version 44 | 45 | LDFLAGS="-X main.dvmCommit=${COMMIT} \ 46 | -X main.dvmVersion=${VERSION}" 47 | 48 | # Build ALL THE THINGS! 49 | if [ "$1" != "all" ]; then 50 | build 51 | else 52 | build windows amd64 53 | build windows 386 54 | build darwin amd64 55 | build darwin 386 56 | build linux amd64 57 | build linux 386 58 | fi 59 | -------------------------------------------------------------------------------- /dvm-helper/checksum/checksum.go: -------------------------------------------------------------------------------- 1 | package checksum 2 | 3 | import ( 4 | "crypto/sha256" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | // CompareChecksum validates the SHA256 checksum for a binary against its checksum file 13 | func CompareChecksum(filePath string, checksumPath string) (bool, error) { 14 | knownChecksum, err := readChecksum(checksumPath) 15 | if err != nil { 16 | return false, err 17 | } 18 | 19 | checksum, err := calculateChecksum(filePath) 20 | if err != nil { 21 | return false, err 22 | } 23 | 24 | return strings.Compare(knownChecksum, checksum) == 0, nil 25 | } 26 | 27 | func readChecksum(checksumPath string) (string, error) { 28 | contents, err := ioutil.ReadFile(checksumPath) 29 | if err != nil { 30 | return "", err 31 | } 32 | checksum := strings.Split(string(contents), " ")[0] 33 | return checksum, nil 34 | } 35 | 36 | func calculateChecksum(filePath string) (string, error) { 37 | file, err := os.Open(filePath) 38 | if err != nil { 39 | return "", err 40 | } 41 | defer file.Close() 42 | 43 | hash := sha256.New() 44 | if _, err := io.Copy(hash, file); err != nil { 45 | return "", err 46 | } 47 | 48 | checksum := hash.Sum(nil) 49 | 50 | // convert from bytes to ascii hex string 51 | return fmt.Sprintf("%x", checksum), nil 52 | } 53 | -------------------------------------------------------------------------------- /dvm-helper/dockerversion/dockerversion.go: -------------------------------------------------------------------------------- 1 | package dockerversion 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "path/filepath" 7 | "sort" 8 | "strings" 9 | 10 | "github.com/Masterminds/semver" 11 | "github.com/howtowhale/dvm/dvm-helper/internal/config" 12 | "github.com/howtowhale/dvm/dvm-helper/internal/downloader" 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | const SystemAlias = "system" 17 | const EdgeAlias = "edge" 18 | 19 | type Version struct { 20 | // Since Docker versions aren't valid versions, (it has leading zeroes) 21 | // We must save the original string representation 22 | raw string 23 | semver *semver.Version 24 | alias string 25 | } 26 | 27 | func NewAlias(alias string, value string) Version { 28 | v := Parse(value) 29 | v.alias = alias 30 | return v 31 | } 32 | 33 | func Parse(value string) Version { 34 | v := Version{raw: value} 35 | semver, err := semver.NewVersion(value) 36 | if err == nil { 37 | v.semver = &semver 38 | } else { 39 | v.alias = value 40 | } 41 | return v 42 | } 43 | 44 | func (version Version) buildDownloadURL(mirror string, forcePrerelease bool) (url string, archived bool, checksumed bool, err error) { 45 | var releaseSlug, versionSlug, extSlug string 46 | 47 | var edgeVersion Version 48 | if version.IsEdge() { 49 | edgeVersion, err = findLatestEdgeVersion(mirror) 50 | if err != nil { 51 | return 52 | } 53 | } 54 | 55 | // Docker Store Download 56 | if version.shouldBeInDockerStore() { 57 | archived = true 58 | checksumed = false 59 | extSlug = archiveFileExt 60 | if mirror == "" { 61 | mirror = "download.docker.com" 62 | } 63 | if version.IsEdge() { 64 | releaseSlug = "edge" 65 | versionSlug = edgeVersion.String() 66 | } else if version.IsPrerelease() || forcePrerelease { 67 | releaseSlug = "test" 68 | versionSlug = version.String() 69 | } else { 70 | releaseSlug = "stable" 71 | versionSlug = version.String() 72 | } 73 | 74 | url = fmt.Sprintf("https://%s/%s/static/%s/%s/docker-%s%s", 75 | mirror, mobyOS, releaseSlug, dockerArch, versionSlug, extSlug) 76 | return 77 | } else { // Original Download 78 | archived = version.shouldBeArchived() 79 | checksumed = true 80 | versionSlug = version.String() 81 | if archived { 82 | extSlug = archiveFileExt 83 | } else { 84 | extSlug = binaryFileExt 85 | } 86 | if mirror == "" { 87 | mirror = "docker.com" 88 | } 89 | if version.IsPrerelease() { 90 | releaseSlug = "test" 91 | } else { 92 | releaseSlug = "get" 93 | } 94 | 95 | url = fmt.Sprintf("https://%s.%s/builds/%s/%s/docker-%s%s", 96 | releaseSlug, mirror, dockerOS, dockerArch, versionSlug, extSlug) 97 | return 98 | } 99 | } 100 | 101 | // Download a Docker release. 102 | // version - the desired version. 103 | // mirrorURL - optional alternate download location. 104 | // binaryPath - full path to where the Docker client binary should be saved. 105 | func (version Version) Download(opts config.DvmOptions, binaryPath string) error { 106 | err := version.download(false, opts, binaryPath) 107 | if err != nil && !version.IsPrerelease() && version.shouldBeInDockerStore() { 108 | // Docker initially publishes non-rc version versions to the test location 109 | // and then later republishes to the stable location 110 | // Retry stable versions against test to find "unstable" stable versions. :-) 111 | opts.Logger.Printf("Could not find a stable release for %s, checking for a test release\n", version) 112 | retryErr := version.download(true, opts, binaryPath) 113 | return errors.Wrapf(retryErr, "Attempted to fallback to downloading from the prerelease location after downloading from the stable location failed: %s", err.Error()) 114 | } 115 | return err 116 | } 117 | 118 | func (version Version) download(forcePrerelease bool, opts config.DvmOptions, binaryPath string) error { 119 | url, archived, checksumed, err := version.buildDownloadURL(opts.MirrorURL, forcePrerelease) 120 | if err != nil { 121 | return errors.Wrapf(err, "Unable to determine the download URL for %s", version) 122 | } 123 | 124 | opts.Logger.Printf("Checking if %s can be found at %s", version, url) 125 | head, err := http.Head(url) 126 | if err != nil { 127 | return errors.Wrapf(err, "Unable to determine if %s is a valid version", version) 128 | } 129 | if head.StatusCode >= 400 { 130 | return errors.Errorf("Version %s not found (%v) - try `dvm ls-remote` to browse available versions", version, head.StatusCode) 131 | } 132 | 133 | d := downloader.New(opts) 134 | binaryName := filepath.Base(binaryPath) 135 | 136 | if archived { 137 | archivedFile := filepath.Join("docker", binaryName) 138 | if checksumed { 139 | return d.DownloadArchivedFileWithChecksum(url, archivedFile, binaryPath) 140 | } 141 | return d.DownloadArchivedFile(url, archivedFile, binaryPath) 142 | } 143 | 144 | if checksumed { 145 | return d.DownloadFileWithChecksum(url, binaryPath) 146 | } 147 | return d.DownloadFile(url, binaryPath) 148 | } 149 | 150 | func (version Version) shouldBeInDockerStore() bool { 151 | if version.IsEdge() { 152 | return true 153 | } 154 | 155 | dockerStoreCutoff, _ := semver.NewVersion("17.06.0-ce") 156 | 157 | return version.semver != nil && !version.semver.LessThan(dockerStoreCutoff) 158 | } 159 | 160 | func (version Version) shouldBeArchived() bool { 161 | archivedReleaseCutoff, _ := semver.NewVersion("1.11.0-rc1") 162 | return version.semver != nil && !version.semver.LessThan(archivedReleaseCutoff) 163 | } 164 | 165 | func (version Version) IsPrerelease() bool { 166 | if version.semver == nil { 167 | return false 168 | } 169 | 170 | tag := version.semver.Prerelease() 171 | 172 | preTags := []string{"rc", "alpha", "beta"} 173 | for i := 0; i < len(preTags); i++ { 174 | if strings.Contains(tag, preTags[i]) { 175 | return true 176 | } 177 | } 178 | 179 | return false 180 | } 181 | 182 | func (version Version) IsEmpty() bool { 183 | return version.semver == nil 184 | } 185 | 186 | func (version Version) IsAlias() bool { 187 | return version.alias != "" 188 | } 189 | 190 | func (version Version) IsSystem() bool { 191 | return version.alias == SystemAlias 192 | } 193 | 194 | func (version *Version) SetAsSystem() { 195 | version.alias = SystemAlias 196 | } 197 | 198 | func (version Version) IsEdge() bool { 199 | return version.alias == EdgeAlias 200 | } 201 | 202 | func (version *Version) SetAsEdge() { 203 | version.alias = EdgeAlias 204 | } 205 | 206 | func (version Version) String() string { 207 | if version.alias != "" && version.semver != nil { 208 | return fmt.Sprintf("%s (%s)", version.alias, version.formatRaw()) 209 | } 210 | return version.formatRaw() 211 | } 212 | 213 | func (version Version) Value() string { 214 | if version.semver == nil { 215 | return "" 216 | } 217 | return version.formatRaw() 218 | } 219 | 220 | // Slug is the path segment under DVM_DIR where the binary is located 221 | func (version Version) Slug() string { 222 | if version.IsSystem() { 223 | return "" 224 | } 225 | if version.IsEdge() { 226 | return version.alias 227 | } 228 | return version.formatRaw() 229 | } 230 | 231 | func (version Version) Name() string { 232 | if version.alias != "" { 233 | return version.alias 234 | } 235 | return version.formatRaw() 236 | } 237 | 238 | func (version Version) formatRaw() string { 239 | value := version.raw 240 | if strings.HasPrefix(strings.ToLower(value), "v") { 241 | value = value[1:] 242 | } 243 | return value 244 | } 245 | 246 | func (version Version) InRange(r string) (bool, error) { 247 | c, err := semver.NewConstraint(r) 248 | if err != nil { 249 | return false, errors.Wrapf(err, "Unable to parse range constraint: %s", r) 250 | } 251 | if version.semver == nil { 252 | return false, nil 253 | } 254 | return c.Matches(*version.semver) == nil, nil 255 | } 256 | 257 | // Compare compares Versions v to o: 258 | // -1 == v is less than o 259 | // 0 == v is equal to o 260 | // 1 == v is greater than o 261 | func (v Version) Compare(o Version) int { 262 | if v.semver != nil && o.semver != nil { 263 | return v.semver.Compare(*o.semver) 264 | } 265 | 266 | return strings.Compare(v.alias, o.alias) 267 | } 268 | 269 | // Equals checks if v is equal to o. 270 | func (v Version) Equals(o Version) bool { 271 | semverMatch := v.Compare(o) == 0 272 | // Enables distinguishing between X.Y.Z and system (X.Y.Z) 273 | systemMatch := v.IsSystem() == o.IsSystem() 274 | aliasmatch := v.alias != "" && v.alias == o.alias 275 | return (semverMatch && systemMatch) || aliasmatch 276 | } 277 | 278 | type Versions []Version 279 | 280 | func (s Versions) Len() int { 281 | return len(s) 282 | } 283 | 284 | func (s Versions) Swap(i, j int) { 285 | s[i], s[j] = s[j], s[i] 286 | } 287 | 288 | func (s Versions) Less(i, j int) bool { 289 | return s[i].Compare(s[j]) == -1 290 | } 291 | 292 | func Sort(versions []Version) { 293 | sort.Sort(Versions(versions)) 294 | } 295 | -------------------------------------------------------------------------------- /dvm-helper/dockerversion/dockerversion.nix.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package dockerversion 4 | 5 | const archiveFileExt string = ".tgz" 6 | const binaryFileExt string = "" 7 | -------------------------------------------------------------------------------- /dvm-helper/dockerversion/dockerversion_386.go: -------------------------------------------------------------------------------- 1 | package dockerversion 2 | 3 | const dockerArch string = "i386" 4 | -------------------------------------------------------------------------------- /dvm-helper/dockerversion/dockerversion_amd64.go: -------------------------------------------------------------------------------- 1 | package dockerversion 2 | 3 | const dockerArch string = "x86_64" 4 | -------------------------------------------------------------------------------- /dvm-helper/dockerversion/dockerversion_arm64.go: -------------------------------------------------------------------------------- 1 | package dockerversion 2 | 3 | const dockerArch string = "aarch64" 4 | -------------------------------------------------------------------------------- /dvm-helper/dockerversion/dockerversion_darwin.go: -------------------------------------------------------------------------------- 1 | package dockerversion 2 | 3 | const dockerOS string = "Darwin" 4 | const mobyOS string = "mac" 5 | -------------------------------------------------------------------------------- /dvm-helper/dockerversion/dockerversion_linux.go: -------------------------------------------------------------------------------- 1 | package dockerversion 2 | 3 | const dockerOS string = "Linux" 4 | const mobyOS string = "linux" 5 | -------------------------------------------------------------------------------- /dvm-helper/dockerversion/dockerversion_test.go: -------------------------------------------------------------------------------- 1 | package dockerversion 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | "path/filepath" 8 | "testing" 9 | 10 | "github.com/howtowhale/dvm/dvm-helper/internal/config" 11 | "github.com/pkg/errors" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestStripLeadingV(t *testing.T) { 16 | v := Parse("v1.0.0") 17 | assert.Equal(t, "1.0.0", v.String(), "Leading v should be stripped from the string representation") 18 | assert.Equal(t, "1.0.0", v.Name(), "Leading v should be stripped from the name") 19 | assert.Equal(t, "1.0.0", v.Value(), "Leading v should be stripped from the version value") 20 | } 21 | 22 | func TestIsPrerelease(t *testing.T) { 23 | var v Version 24 | 25 | v = Parse("17.3.0-ce-rc1") 26 | assert.True(t, v.IsPrerelease(), "%s should be a prerelease", v) 27 | 28 | v = Parse("1.12.4-rc1") 29 | assert.True(t, v.IsPrerelease(), "%s should be a prerelease", v) 30 | 31 | v = Parse("1.12.4-beta.1") 32 | assert.True(t, v.IsPrerelease(), "%s should be a prerelease", v) 33 | 34 | v = Parse("1.12.4-alpha-2") 35 | assert.True(t, v.IsPrerelease(), "%s should be a prerelease", v) 36 | 37 | v = Parse("17.3.0-ce") 38 | assert.False(t, v.IsPrerelease(), "%s should NOT be a prerelease", v) 39 | } 40 | 41 | func TestLeadingZeroInVersion(t *testing.T) { 42 | v := Parse("v17.03.0-ce") 43 | 44 | assert.Equal(t, "17.03.0-ce", v.String(), "Leading zeroes in the version should be preserved") 45 | } 46 | 47 | func TestSystemAlias(t *testing.T) { 48 | v := Parse(SystemAlias) 49 | assert.Empty(t, v.Slug(), 50 | "The system alias should not have a slug") 51 | assert.Equal(t, SystemAlias, v.String(), 52 | "An empty alias should only print the alias") 53 | assert.Equal(t, SystemAlias, v.Name(), 54 | "The name for an aliased version should be its alias") 55 | assert.Equal(t, "", v.Value(), 56 | "The value for an empty aliased version should be empty") 57 | } 58 | 59 | func TestEdgeAlias(t *testing.T) { 60 | v := Parse(EdgeAlias) 61 | assert.Equal(t, EdgeAlias, v.Slug(), 62 | "The slug for the edge version should be 'edge'") 63 | assert.Equal(t, EdgeAlias, v.String(), 64 | "An empty alias should only print the alias") 65 | assert.Equal(t, EdgeAlias, v.Name(), 66 | "The name for an aliased version should be its alias") 67 | assert.Equal(t, "", v.Value(), 68 | "The value for an empty aliased version should be empty") 69 | } 70 | 71 | func TestEdgeAliasWithVersion(t *testing.T) { 72 | v := Parse("17.06.0-ce+02c1d87") 73 | v.SetAsEdge() 74 | assert.Equal(t, EdgeAlias, v.Slug(), 75 | "The slug for the edge version should be 'edge'") 76 | assert.Equal(t, "edge (17.06.0-ce+02c1d87)", v.String(), 77 | "The string representation should include the alias and version") 78 | assert.Equal(t, EdgeAlias, v.Name(), 79 | "The name for an aliased version should be its alias") 80 | assert.Equal(t, "17.06.0-ce+02c1d87", v.Value(), 81 | "The value for a populated alias should be the version") 82 | } 83 | 84 | func TestAlias(t *testing.T) { 85 | v := NewAlias("prod", "1.2.3") 86 | assert.Equal(t, "1.2.3", v.Slug(), 87 | "The slug for an aliased version should be its semver value") 88 | assert.Equal(t, "prod (1.2.3)", v.String(), 89 | "The string representation for an aliased version should include both alias and version") 90 | assert.Equal(t, "prod", v.Name(), 91 | "The name for an aliased version should be its alias") 92 | assert.Equal(t, "1.2.3", v.Value(), 93 | "The value for an aliased version should be its semver value") 94 | } 95 | 96 | func TestSemanticVersion(t *testing.T) { 97 | v := Parse("1.2.3") 98 | assert.Equal(t, "1.2.3", v.Slug(), 99 | "The slug for a a semantic version should be its semver value") 100 | assert.Equal(t, "1.2.3", v.String(), 101 | "The string representation for a semantic version should only include the semver value") 102 | assert.Equal(t, "1.2.3", v.Name(), 103 | "The name for a semantic version should be its semver value") 104 | assert.Equal(t, "1.2.3", v.Value(), 105 | "The value for a semantic version should be its semver value") 106 | } 107 | 108 | func TestSetAsEdge(t *testing.T) { 109 | v := Parse("1.2.3") 110 | v.SetAsEdge() 111 | assert.True(t, v.IsEdge()) 112 | } 113 | 114 | func TestSetAsSystem(t *testing.T) { 115 | v := Parse("1.2.3") 116 | v.SetAsSystem() 117 | assert.True(t, v.IsSystem()) 118 | } 119 | 120 | func TestVersion_BuildDownloadURL(t *testing.T) { 121 | testcases := map[Version]struct { 122 | wantURL string 123 | wantArchived bool 124 | wantChecksum bool 125 | }{ 126 | // original download location, without compression 127 | Parse("1.10.3"): { 128 | wantURL: fmt.Sprintf("https://get.docker.com/builds/%s/%s/docker-1.10.3", dockerOS, dockerArch), 129 | wantArchived: false, 130 | wantChecksum: true, 131 | }, 132 | 133 | // original download location, without compression, prerelease 134 | /* test.docker.com has been removed by docker 135 | Parse("1.10.0-rc1"): { 136 | wantURL: fmt.Sprintf("https://test.docker.com/builds/%s/%s/docker-1.10.0-rc1", dockerOS, dockerArch), 137 | wantArchived: false, 138 | wantChecksum: true, 139 | }, 140 | */ 141 | 142 | // compressed binaries 143 | /* test.docker.com has been removed by docker 144 | Parse("1.11.0-rc1"): { 145 | wantURL: fmt.Sprintf("https://test.docker.com/builds/%s/%s/docker-1.11.0-rc1.tgz", dockerOS, dockerArch), 146 | wantArchived: true, 147 | wantChecksum: true, 148 | }, 149 | */ 150 | 151 | // original version scheme, prerelease binaries 152 | /* test.docker.com has been removed by docker 153 | Parse("1.13.0-rc1"): { 154 | wantURL: fmt.Sprintf("https://test.docker.com/builds/%s/%s/docker-1.13.0-rc1.tgz", dockerOS, dockerArch), 155 | wantArchived: true, 156 | wantChecksum: true, 157 | }, 158 | */ 159 | 160 | // yearly notation, original download location, release location 161 | Parse("17.03.0-ce"): { 162 | wantURL: fmt.Sprintf("https://get.docker.com/builds/%s/%s/docker-17.03.0-ce%s", dockerOS, dockerArch, archiveFileExt), 163 | wantArchived: true, 164 | wantChecksum: true, 165 | }, 166 | 167 | // docker store download (no more checksums) 168 | Parse("17.06.0-ce"): { 169 | wantURL: fmt.Sprintf("https://download.docker.com/%s/static/stable/%s/docker-17.06.0-ce.tgz", mobyOS, dockerArch), 170 | wantArchived: true, 171 | wantChecksum: false, 172 | }, 173 | 174 | // docker store download, prerelease 175 | Parse("17.07.0-ce-rc1"): { 176 | wantURL: fmt.Sprintf("https://download.docker.com/%s/static/test/%s/docker-17.07.0-ce-rc1.tgz", mobyOS, dockerArch), 177 | wantArchived: true, 178 | wantChecksum: false, 179 | }, 180 | } 181 | 182 | for version, testcase := range testcases { 183 | t.Run(version.String(), func(t *testing.T) { 184 | gotURL, gotArchived, gotChecksumed, err := version.buildDownloadURL("", false) 185 | if err != nil { 186 | t.Fatal(err) 187 | } 188 | 189 | if testcase.wantURL != gotURL { 190 | t.Fatalf("Expected %s to be downloaded from '%s', but got '%s'", version, testcase.wantURL, gotURL) 191 | } 192 | if testcase.wantArchived != gotArchived { 193 | t.Fatalf("Expected archive for %s to be %v, got %v", version, testcase.wantArchived, gotArchived) 194 | } 195 | if testcase.wantChecksum != gotChecksumed { 196 | t.Fatalf("Expected checksum for %s to be %v, got %v", version, testcase.wantChecksum, gotChecksumed) 197 | } 198 | 199 | response, err := http.DefaultClient.Head(gotURL) 200 | if err != nil { 201 | t.Fatalf("%#v", errors.Wrapf(err, "Unable to download release from %s", gotURL)) 202 | } 203 | 204 | if response.StatusCode != 200 { 205 | t.Fatalf("Unexpected status code (%d) when downloading %s", response.StatusCode, gotURL) 206 | } 207 | }) 208 | } 209 | } 210 | 211 | func TestVersion_DownloadEdgeRelease(t *testing.T) { 212 | version := Parse("edge") 213 | tempDir, _ := ioutil.TempDir("", "dvmtest") 214 | opts := config.NewDvmOptions() 215 | opts.DvmDir = filepath.Join(tempDir, ".dvm") 216 | destPath := filepath.Join(opts.DvmDir, "docker") 217 | 218 | err := version.Download(opts, destPath) 219 | if err != nil { 220 | t.Fatalf("%#v", err) 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /dvm-helper/dockerversion/dockerversion_windows.go: -------------------------------------------------------------------------------- 1 | package dockerversion 2 | 3 | const dockerOS string = "Windows" 4 | const mobyOS string = "win" 5 | const archiveFileExt string = ".zip" 6 | const binaryFileExt string = ".exe" 7 | -------------------------------------------------------------------------------- /dvm-helper/dockerversion/edge_version.go: -------------------------------------------------------------------------------- 1 | package dockerversion 2 | 3 | func findLatestEdgeVersion(mirrorURL string) (Version, error) { 4 | results, err := ListVersions(mirrorURL, Edge) 5 | if err != nil { 6 | return Version{}, err 7 | } 8 | 9 | last := len(results) - 1 10 | return results[last], nil 11 | } 12 | -------------------------------------------------------------------------------- /dvm-helper/dockerversion/edge_version_test.go: -------------------------------------------------------------------------------- 1 | package dockerversion 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/howtowhale/dvm/dvm-helper/internal/test" 10 | ) 11 | 12 | func TestVersion_findLatestEdgeVersion(t *testing.T) { 13 | releaseListing := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 14 | w.Header().Set("Content-Type", "text/html") 15 | fmt.Fprintln(w, test.LoadTestData("edge_releases.html")) 16 | })) 17 | 18 | v, err := findLatestEdgeVersion(releaseListing.URL) 19 | if err != nil { 20 | t.Fatalf("%#v", err) 21 | } 22 | 23 | wantV := "17.06.0-ce" 24 | gotV := v.String() 25 | if wantV != gotV { 26 | t.Fatalf("Expected '%s', got '%s'", wantV, gotV) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /dvm-helper/dockerversion/query.go: -------------------------------------------------------------------------------- 1 | package dockerversion 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | 9 | "regexp" 10 | 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | type ReleaseType string 15 | 16 | const ( 17 | Edge ReleaseType = "edge" 18 | Test ReleaseType = "test" 19 | Stable ReleaseType = "stable" 20 | ) 21 | 22 | var hrefRegex = regexp.MustCompile(fmt.Sprintf(`href="docker-(.*)\%s"`, archiveFileExt)) 23 | 24 | func ListVersions(mirrorURL string, releaseType ReleaseType) ([]Version, error) { 25 | if mirrorURL == "" { 26 | mirrorURL = "https://download.docker.com" 27 | } 28 | 29 | mirror, err := url.Parse(mirrorURL) 30 | if err != nil { 31 | return nil, errors.Wrapf(err, "Unable to parse the mirror URL: %s", mirrorURL) 32 | } 33 | 34 | indexURL := fmt.Sprintf("%s://%s/%s/static/%s/%s", mirror.Scheme, mirror.Host, mobyOS, releaseType, dockerArch) 35 | response, err := http.Get(indexURL) 36 | if err != nil { 37 | return nil, errors.Wrapf(err, "Unable to list %s releases at %s", releaseType, indexURL) 38 | } 39 | defer response.Body.Close() 40 | 41 | b := bytes.Buffer{} 42 | _, err = b.ReadFrom(response.Body) 43 | if err != nil { 44 | } 45 | errors.Wrapf(err, "Unable to read the listing of %s releases at %s", releaseType, indexURL) 46 | 47 | matches := hrefRegex.FindAllStringSubmatch(b.String(), -1) 48 | var results []Version 49 | for _, match := range matches { 50 | version := Parse(match[1]) 51 | if version.semver == nil { 52 | continue 53 | } 54 | results = append(results, version) 55 | } 56 | 57 | if len(results) == 0 { 58 | return nil, errors.Errorf("No valid %s versions were found at %s", releaseType, indexURL) 59 | } 60 | 61 | Sort(results) 62 | 63 | return results, nil 64 | } 65 | -------------------------------------------------------------------------------- /dvm-helper/dockerversion/testdata/edge_releases.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Index of /linux/static/edge/x86_64/ 6 | 7 | 8 |

Index of /linux/static/edge/x86_64/

9 |
10 |
../
11 | docker-17.03.0-ce.tgz  2017-03-01 11:46  27M
12 | docker-17.04.0-ce.tgz  2017-04-06 01:07  26M
13 | docker-17.05.0-ce.tgz  2017-05-05 04:11  28M
14 | docker-17.06.0-ce.tgz  2017-06-28 04:51  29M
15 | 

-------------------------------------------------------------------------------- /dvm-helper/dvm-helper.386.go: -------------------------------------------------------------------------------- 1 | //go:build 386 2 | // +build 386 3 | 4 | package main 5 | 6 | const dvmArch string = "i386" 7 | -------------------------------------------------------------------------------- /dvm-helper/dvm-helper.amd64.go: -------------------------------------------------------------------------------- 1 | //go:build amd64 2 | // +build amd64 3 | 4 | package main 5 | 6 | const dvmArch string = "x86_64" 7 | -------------------------------------------------------------------------------- /dvm-helper/dvm-helper.arm64.go: -------------------------------------------------------------------------------- 1 | //go:build arm64 2 | // +build arm64 3 | 4 | package main 5 | 6 | const dvmArch string = "aarch64" 7 | -------------------------------------------------------------------------------- /dvm-helper/dvm-helper.darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | // +build darwin 3 | 4 | package main 5 | 6 | const dvmOS string = "Darwin" 7 | -------------------------------------------------------------------------------- /dvm-helper/dvm-helper.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | neturl "net/url" 9 | "os" 10 | "os/exec" 11 | "path/filepath" 12 | "regexp" 13 | "strings" 14 | 15 | "github.com/Masterminds/semver" 16 | "github.com/codegangsta/cli" 17 | dockerclient "github.com/docker/docker/client" 18 | "github.com/fatih/color" 19 | "github.com/google/go-github/github" 20 | "github.com/howtowhale/dvm/dvm-helper/dockerversion" 21 | "github.com/howtowhale/dvm/dvm-helper/internal/config" 22 | "github.com/howtowhale/dvm/dvm-helper/url" 23 | "github.com/pkg/errors" 24 | "github.com/ryanuber/go-glob" 25 | "golang.org/x/oauth2" 26 | ) 27 | 28 | // These are global command line variables 29 | var opts = config.NewDvmOptions() 30 | 31 | // This is a nasty global state flag that we flip. Bad Carolyn. 32 | var useAfterInstall bool 33 | 34 | // These are set during the build 35 | var dvmVersion string 36 | var dvmCommit string 37 | var upgradeDisabled string // Allow package managers like homebrew to disable in-place upgrades 38 | var githubUrlOverride string 39 | 40 | const ( 41 | retCodeInvalidArgument = 127 42 | retCodeInvalidOperation = 3 43 | retCodeRuntimeError = 1 44 | versionEnvVar = "DOCKER_VERSION" 45 | ) 46 | 47 | func main() { 48 | app := makeCliApp() 49 | app.Run(os.Args) 50 | } 51 | 52 | func makeCliApp() *cli.App { 53 | app := cli.NewApp() 54 | app.Name = "Docker Version Manager" 55 | app.Usage = "Manage multiple versions of the Docker client" 56 | app.Version = fmt.Sprintf("%s (%s)", dvmVersion, dvmCommit) 57 | app.EnableBashCompletion = true 58 | app.Flags = []cli.Flag{ 59 | cli.StringFlag{Name: "github-token", EnvVar: "GITHUB_TOKEN", Usage: "Increase the github api rate limit by specifying your github personal access token."}, 60 | cli.StringFlag{Name: "dvm-dir", EnvVar: "DVM_DIR", Usage: "Specify an alternate DVM home directory, defaults to ~/.dvm."}, 61 | cli.StringFlag{Name: "shell", EnvVar: "SHELL", Usage: "Specify the shell format in which environment variables should be output, e.g. powershell, cmd or sh/bash. Defaults to sh/bash."}, 62 | cli.BoolFlag{Name: "debug", Usage: "Print additional debug information."}, 63 | cli.BoolFlag{Name: "silent", EnvVar: "DVM_SILENT", Usage: "Suppress output. Errors will still be displayed."}, 64 | } 65 | app.Commands = []cli.Command{ 66 | { 67 | Name: "detect", 68 | Usage: "Detect the appropriate Docker client version", 69 | Action: func(c *cli.Context) error { 70 | setGlobalVars(c) 71 | 72 | writeDebug("dvm detect") 73 | detect() 74 | return nil 75 | }, 76 | }, 77 | { 78 | Name: "install", 79 | Aliases: []string{"i"}, 80 | Usage: "dvm install [], dvm install edge\n\tInstall a Docker version, using $DOCKER_VERSION if the version is not specified.", 81 | Flags: []cli.Flag{ 82 | cli.StringFlag{Name: "mirror-url", EnvVar: "DVM_MIRROR_URL", Usage: "Specify an alternate URL from which to download the Docker client. Defaults to https://get.docker.com/builds"}, 83 | }, 84 | Action: func(c *cli.Context) error { 85 | setGlobalVars(c) 86 | 87 | value := c.Args().First() 88 | if value == "" { 89 | value = getDockerVersionVar() 90 | 91 | if value == "" { 92 | die("The install command requires that a version is specified or the DOCKER_VERSION environment variable is set.", nil, retCodeInvalidArgument) 93 | } 94 | } 95 | 96 | writeDebug("dvm install %s", value) 97 | install(dockerversion.Parse(value)) 98 | return nil 99 | }, 100 | }, 101 | { 102 | Name: "uninstall", 103 | Usage: "dvm uninstall \n\tUninstall a Docker version.", 104 | Action: func(c *cli.Context) error { 105 | setGlobalVars(c) 106 | 107 | value := c.Args().First() 108 | if value == "" { 109 | die("The uninstall command requires that a version is specified.", nil, retCodeInvalidArgument) 110 | } 111 | 112 | writeDebug("dvm uninstall %s", value) 113 | uninstall(dockerversion.Parse(value)) 114 | return nil 115 | }, 116 | }, 117 | { 118 | Name: "use", 119 | Usage: "dvm use [], dvm use system, dvm use edge\n\tUse a Docker version, using $DOCKER_VERSION if the version is not specified.", 120 | Flags: []cli.Flag{ 121 | cli.StringFlag{Name: "mirror-url", EnvVar: "DVM_MIRROR_URL", Usage: "Specify an alternate URL from which to download the Docker client. Defaults to https://get.docker.com/builds"}, 122 | cli.BoolFlag{Name: "nocheck", EnvVar: "DVM_NOCHECK", Usage: "Do not check if version exists (use with caution)."}, 123 | }, 124 | Action: func(c *cli.Context) error { 125 | setGlobalVars(c) 126 | 127 | value := c.Args().First() 128 | if value == "" { 129 | value = getDockerVersionVar() 130 | 131 | if value == "" { 132 | die("The use command requires that a version is specified or the DOCKER_VERSION environment variable is set.", nil, retCodeInvalidOperation) 133 | } 134 | } 135 | 136 | writeDebug("dvm use %s", value) 137 | use(dockerversion.Parse(value)) 138 | return nil 139 | }, 140 | }, 141 | { 142 | Name: "deactivate", 143 | Usage: "dvm deactivate\n\tUndo the effects of `dvm` on current shell.", 144 | Action: func(c *cli.Context) error { 145 | setGlobalVars(c) 146 | 147 | writeDebug("dvm deactivate") 148 | deactivate() 149 | return nil 150 | }, 151 | }, 152 | { 153 | Name: "current", 154 | Usage: "dvm current\n\tPrint the current Docker version.", 155 | Action: func(c *cli.Context) error { 156 | setGlobalVars(c) 157 | 158 | writeDebug("dvm current") 159 | current() 160 | return nil 161 | }, 162 | }, 163 | { 164 | Name: "which", 165 | Usage: "dvm which\n\tPrint the path to the current Docker version.", 166 | Action: func(c *cli.Context) error { 167 | setGlobalVars(c) 168 | 169 | writeDebug("dvm which") 170 | which() 171 | return nil 172 | }, 173 | }, 174 | { 175 | Name: "alias", 176 | Usage: "dvm alias \n\tCreate an alias to a Docker version.", 177 | Action: func(c *cli.Context) error { 178 | setGlobalVars(c) 179 | 180 | name := c.Args().Get(0) 181 | value := c.Args().Get(1) 182 | if name == "" || value == "" { 183 | die("The alias command requires both an alias name and a version.", nil, retCodeInvalidArgument) 184 | } 185 | 186 | writeDebug("dvm alias %s %s", name, value) 187 | alias(name, value) 188 | return nil 189 | }, 190 | }, 191 | { 192 | Name: "unalias", 193 | Usage: "dvm unalias \n\tRemove a Docker version alias.", 194 | Action: func(c *cli.Context) error { 195 | setGlobalVars(c) 196 | 197 | alias := c.Args().First() 198 | if alias == "" { 199 | die("The unalias command requires an alias alias.", nil, retCodeInvalidArgument) 200 | } 201 | 202 | writeDebug("dvm unalias %s", alias) 203 | unalias(alias) 204 | return nil 205 | }, 206 | }, 207 | { 208 | Name: "list", 209 | Aliases: []string{"ls"}, 210 | Usage: "dvm list []\n\tList installed Docker versions.", 211 | Action: func(c *cli.Context) error { 212 | setGlobalVars(c) 213 | 214 | pattern := c.Args().First() 215 | 216 | writeDebug("dvm list %s", pattern) 217 | list(pattern) 218 | return nil 219 | }, 220 | }, 221 | { 222 | Name: "list-remote", 223 | Aliases: []string{"ls-remote"}, 224 | Usage: "dvm list-remote []\n\tList available Docker versions.", 225 | Flags: []cli.Flag{ 226 | cli.BoolFlag{Name: "pre", Usage: "Include pre-release versions"}, 227 | }, 228 | Action: func(c *cli.Context) error { 229 | setGlobalVars(c) 230 | 231 | pattern := c.Args().First() 232 | 233 | writeDebug("dvm list-remote %s", pattern) 234 | listRemote(pattern) 235 | return nil 236 | }, 237 | }, 238 | { 239 | Name: "list-alias", 240 | Aliases: []string{"ls-alias"}, 241 | Usage: "dvm list-alias\n\tList Docker version aliases.", 242 | Action: func(c *cli.Context) error { 243 | setGlobalVars(c) 244 | 245 | writeDebug("dvm list-alias") 246 | listAlias() 247 | return nil 248 | }, 249 | }, 250 | } 251 | 252 | if upgradeDisabled != "true" { 253 | app.Commands = append(app.Commands, cli.Command{ 254 | Name: "upgrade", 255 | Usage: "dvm upgrade\n\tUpgrade dvm to the latest release.", 256 | Flags: []cli.Flag{ 257 | cli.BoolFlag{Name: "check", Usage: "Checks if an newer version of dvm is available, but does not perform the upgrade."}, 258 | cli.StringFlag{Name: "version", Usage: "Upgrade to the specified version."}, 259 | }, 260 | Action: func(c *cli.Context) error { 261 | setGlobalVars(c) 262 | upgrade(c.Bool("check"), c.String("version")) 263 | return nil 264 | }, 265 | }) 266 | } 267 | 268 | return app 269 | } 270 | 271 | func setGlobalVars(c *cli.Context) { 272 | useAfterInstall = true 273 | 274 | opts.Debug = c.GlobalBool("debug") 275 | if opts.Debug { 276 | opts.Logger = log.New(color.Output, "", log.LstdFlags) 277 | } else { 278 | opts.Logger = log.New(ioutil.Discard, "", log.LstdFlags) 279 | } 280 | 281 | opts.Token = c.GlobalString("github-token") 282 | opts.Shell = c.GlobalString("shell") 283 | validateShellFlag() 284 | 285 | opts.Silent = c.GlobalBool("silent") 286 | opts.MirrorURL = c.String("mirror-url") 287 | opts.IncludePrereleases = c.Bool("pre") 288 | 289 | opts.DvmDir = c.GlobalString("dvm-dir") 290 | if opts.DvmDir == "" { 291 | opts.DvmDir = filepath.Join(getUserHomeDir(), ".dvm") 292 | } 293 | writeDebug("The dvm home directory is: %s", opts.DvmDir) 294 | } 295 | 296 | func detect() { 297 | writeDebug("dvm detect") 298 | 299 | docker, err := dockerclient.NewEnvClient() 300 | if err != nil { 301 | die("Cannot build a docker client from environment variables", err, retCodeRuntimeError) 302 | } 303 | 304 | versionResult, err := docker.ServerVersion(context.Background()) 305 | if err != nil { 306 | die("Unable to query docker version", err, retCodeRuntimeError) 307 | } 308 | 309 | writeDebug("Queried /version and got Version: %s", versionResult.Version) 310 | version := dockerversion.Parse(versionResult.Version) 311 | 312 | // Docker versions prior to 1.12 don't return a usable client version 313 | // Lookup the client version from the API version 314 | if version.IsEmpty() { 315 | writeDebug("Attempting to lookup a client version for API version: %s", versionResult.APIVersion) 316 | 317 | // api version -> client version range 318 | oldVersionMap := map[string]string{ 319 | "1.23": "1.11.x", 320 | "1.22": "1.10.x", 321 | "1.21": "1.9.x", 322 | "1.20": "1.8.x", 323 | "1.19": "1.7.x", 324 | "1.18": "1.6.x", 325 | } 326 | clientRange, found := oldVersionMap[versionResult.APIVersion] 327 | if !found { 328 | die("Unable to detect the proper client version for Docker API version %s", nil, retCodeRuntimeError, versionResult.APIVersion) 329 | } 330 | 331 | // Find the highest version that satisfies the client version range 332 | availableVersions := getAvailableVersions("", true) 333 | for i := len(availableVersions) - 1; i >= 0; i-- { 334 | v := availableVersions[i] 335 | 336 | if ok, _ := v.InRange(clientRange); ok { 337 | version = v 338 | break 339 | } 340 | } 341 | if version.IsEmpty() { 342 | die("Unable to detect the proper client version for %s", nil, retCodeRuntimeError, clientRange) 343 | } 344 | } 345 | writeDebug("Detected client version: %s", version) 346 | 347 | os.Setenv(versionEnvVar, version.String()) 348 | writeEnvironmentVariableScript(versionEnvVar) 349 | 350 | use(version) 351 | } 352 | 353 | func upgrade(checkOnly bool, version string) { 354 | if version != "" && dvmVersion == version { 355 | writeWarning("dvm %s is already installed.", version) 356 | return 357 | } 358 | 359 | if version == "" { 360 | shouldUpgrade, latestVersion := isUpgradeAvailable() 361 | if !shouldUpgrade { 362 | writeInfo("The latest version of dvm is already installed.") 363 | return 364 | } 365 | 366 | version = latestVersion 367 | } 368 | 369 | if checkOnly { 370 | writeInfo("dvm %s is available. Run `dvm upgrade` to install the latest version.", version) 371 | return 372 | } 373 | 374 | writeInfo("Upgrading to dvm %s...", version) 375 | upgradeSelf(version) 376 | } 377 | 378 | func buildDvmReleaseURL(version string, elem ...string) string { 379 | prefix := url.Join("https://howtowhale.github.io/dvm/downloads", version) 380 | suffix := url.Join(elem...) 381 | return url.Join(prefix, suffix) 382 | } 383 | 384 | func current() { 385 | current, err := getCurrentDockerVersion() 386 | if err != nil { 387 | writeWarning("N/A") 388 | } else { 389 | writeInfo(current.String()) 390 | } 391 | } 392 | 393 | func list(pattern string) { 394 | pattern += "*" 395 | versions := getInstalledVersions(pattern) 396 | current, _ := getCurrentDockerVersion() 397 | 398 | for _, version := range versions { 399 | if current.String() == version.String() { 400 | color.Green("->\t%s", version) 401 | } else { 402 | writeInfo("\t%s", version) 403 | } 404 | } 405 | } 406 | 407 | func install(version dockerversion.Version) { 408 | versionDir := getVersionDir(version) 409 | 410 | if version.IsEdge() { 411 | // Always install latest of edge build 412 | err := os.RemoveAll(versionDir) 413 | if err != nil { 414 | die("Unable to remove edge version at %s.", err, retCodeRuntimeError, versionDir) 415 | } 416 | } 417 | 418 | if _, err := os.Stat(versionDir); err == nil { 419 | writeWarning("%s is already installed", version) 420 | use(version) 421 | return 422 | } 423 | 424 | writeInfo("Installing %s...", version) 425 | 426 | downloadRelease(version) 427 | 428 | if useAfterInstall { 429 | use(version) 430 | } 431 | } 432 | 433 | func downloadRelease(version dockerversion.Version) { 434 | destPath := filepath.Join(getVersionDir(version), getBinaryName()) 435 | err := version.Download(opts, destPath) 436 | if err != nil { 437 | die("", err, retCodeRuntimeError) 438 | } 439 | 440 | writeDebug("Downloaded Docker %s to %s", version, destPath) 441 | } 442 | 443 | func uninstall(version dockerversion.Version) { 444 | current, _ := getCurrentDockerVersion() 445 | if current.Equals(version) { 446 | die("Cannot uninstall the currently active Docker version.", nil, retCodeInvalidOperation) 447 | } 448 | 449 | versionDir := getVersionDir(version) 450 | if _, err := os.Stat(versionDir); os.IsNotExist(err) { 451 | writeWarning("%s is not installed.", version) 452 | return 453 | } 454 | 455 | err := os.RemoveAll(versionDir) 456 | if err != nil { 457 | die("Unable to uninstall Docker version %s located in %s.", err, retCodeRuntimeError, version, versionDir) 458 | } 459 | 460 | writeInfo("Uninstalled Docker %s.", version) 461 | } 462 | 463 | func use(version dockerversion.Version) { 464 | if version.IsAlias() && aliasExists(version.Name()) { 465 | aliasedVersion, _ := ioutil.ReadFile(getAliasPath(version.Name())) 466 | version = dockerversion.NewAlias(version.Name(), string(aliasedVersion)) 467 | writeDebug("Using alias: %s", version) 468 | } 469 | 470 | useAfterInstall = false 471 | ensureVersionIsInstalled(version) 472 | 473 | if version.IsSystem() { 474 | version, _ = getSystemDockerVersion() 475 | } else if version.IsEdge() { 476 | version, _ = getEdgeDockerVersion() 477 | } 478 | 479 | removePreviousDockerVersionFromPath() 480 | if !version.IsSystem() { 481 | prependDockerVersionToPath(version) 482 | } 483 | 484 | writeEnvironmentVariableScript(pathEnvVar) 485 | writeInfo("Now using Docker %s", version) 486 | } 487 | 488 | func which() { 489 | currentPath, err := getCurrentDockerPath() 490 | if err == nil { 491 | writeInfo(currentPath) 492 | } 493 | } 494 | 495 | func alias(alias string, value string) { 496 | version := dockerversion.NewAlias(alias, value) 497 | if !isVersionInstalled(version) { 498 | die("The aliased version, %s, is not installed.", nil, retCodeInvalidArgument, version) 499 | } 500 | 501 | aliasPath := getAliasPath(alias) 502 | if _, err := os.Stat(aliasPath); err == nil { 503 | writeDebug("Overwriting existing alias.") 504 | } 505 | 506 | writeFile(aliasPath, version.Value()) 507 | writeInfo("Aliased %s to %s.", alias, value) 508 | } 509 | 510 | func unalias(alias string) { 511 | if !aliasExists(alias) { 512 | writeWarning("%s is not an alias.", alias) 513 | return 514 | } 515 | 516 | aliasPath := getAliasPath(alias) 517 | err := os.Remove(aliasPath) 518 | if err != nil { 519 | die("Unable to remove alias %s at %s.", err, retCodeRuntimeError, alias, aliasPath) 520 | } 521 | 522 | writeInfo("Removed alias %s", alias) 523 | } 524 | 525 | func listAlias() { 526 | aliases := getAliases() 527 | for alias, version := range aliases { 528 | writeInfo("\t%s -> %s", alias, version) 529 | } 530 | } 531 | 532 | func aliasExists(alias string) bool { 533 | aliasPath := getAliasPath(alias) 534 | if _, err := os.Stat(aliasPath); err == nil { 535 | return true 536 | } 537 | 538 | return false 539 | } 540 | 541 | func getAliases() map[string]string { 542 | aliases, _ := filepath.Glob(getAliasPath("*")) 543 | 544 | results := make(map[string]string) 545 | for _, aliasPath := range aliases { 546 | alias := filepath.Base(aliasPath) 547 | version, err := ioutil.ReadFile(aliasPath) 548 | if err != nil { 549 | writeDebug("Excluding alias: %s.", err, retCodeRuntimeError, alias) 550 | continue 551 | } 552 | 553 | results[alias] = string(version) 554 | } 555 | 556 | return results 557 | } 558 | 559 | func getAliasPath(alias string) string { 560 | return filepath.Join(opts.DvmDir, "alias", alias) 561 | } 562 | 563 | func getBinaryName() string { 564 | return "docker" + binaryFileExt 565 | } 566 | 567 | func deactivate() { 568 | removePreviousDockerVersionFromPath() 569 | writeEnvironmentVariableScript(pathEnvVar) 570 | } 571 | 572 | func prependDockerVersionToPath(version dockerversion.Version) { 573 | prependPath(getVersionDir(version)) 574 | } 575 | 576 | func writeEnvironmentVariableScript(name string) { 577 | // Write to a shell script for the calling wrapper to execute which exports the environment variable 578 | scriptPath := buildDvmOutputScriptPath() 579 | contents := exportEnvironmentVariable(name) 580 | 581 | writeFile(scriptPath, contents) 582 | } 583 | 584 | func buildDvmOutputScriptPath() string { 585 | var fileExtension string 586 | if opts.Shell == "powershell" { 587 | fileExtension = "ps1" 588 | } else if opts.Shell == "cmd" { 589 | fileExtension = "cmd" 590 | } else { // default to bash 591 | fileExtension = "sh" 592 | } 593 | return filepath.Join(opts.DvmDir, ".tmp", "dvm-output."+fileExtension) 594 | } 595 | 596 | func removePreviousDockerVersionFromPath() { 597 | removePath(getCleanPathRegex()) 598 | } 599 | 600 | func ensureVersionIsInstalled(version dockerversion.Version) { 601 | if isVersionInstalled(version) { 602 | return 603 | } 604 | 605 | writeInfo("%s is not installed. Installing now...", version) 606 | install(version) 607 | } 608 | 609 | func isVersionInstalled(version dockerversion.Version) bool { 610 | writeDebug("Checking if version is installed: %s", version) 611 | installedVersions := getInstalledVersions("*") 612 | 613 | for _, availableVersion := range installedVersions { 614 | if version.Equals(availableVersion) { 615 | writeDebug("Version %s is installed", version) 616 | return true 617 | } 618 | } 619 | 620 | return false 621 | } 622 | 623 | func getCurrentDockerPath() (string, error) { 624 | currentDockerPath, err := exec.LookPath("docker") 625 | return currentDockerPath, err 626 | } 627 | 628 | func getCurrentDockerVersion() (dockerversion.Version, error) { 629 | currentDockerPath, err := getCurrentDockerPath() 630 | if err != nil { 631 | return dockerversion.Version{}, err 632 | } 633 | 634 | systemDockerPath, _ := getSystemDockerPath() 635 | edgeVersionPath, _ := getEdgeDockerPath() 636 | 637 | isSystem := currentDockerPath == systemDockerPath 638 | isEdge := currentDockerPath == edgeVersionPath 639 | 640 | current, _ := getDockerVersion(currentDockerPath, isEdge) 641 | 642 | if isSystem { 643 | writeDebug("The current docker is the system installation") 644 | current.SetAsSystem() 645 | } 646 | 647 | if isEdge { 648 | writeDebug("The current docker is an edge version") 649 | current.SetAsEdge() 650 | } 651 | 652 | writeDebug("The current version is: %s", current) 653 | return current, nil 654 | } 655 | 656 | func getSystemDockerPath() (string, error) { 657 | originalPath := getPath() 658 | removePreviousDockerVersionFromPath() 659 | systemDockerPath, err := exec.LookPath("docker") 660 | setPath(originalPath) 661 | return systemDockerPath, err 662 | } 663 | 664 | func getSystemDockerVersion() (dockerversion.Version, error) { 665 | systemDockerPath, err := getSystemDockerPath() 666 | if err != nil { 667 | return dockerversion.Version{}, err 668 | } 669 | version, err := getDockerVersion(systemDockerPath, false) 670 | version.SetAsSystem() 671 | return version, err 672 | } 673 | 674 | func getEdgeDockerPath() (string, error) { 675 | edgeVersionPath := filepath.Join(getVersionsDir(), dockerversion.EdgeAlias, getBinaryName()) 676 | _, err := os.Stat(edgeVersionPath) 677 | return edgeVersionPath, err 678 | } 679 | 680 | func getEdgeDockerVersion() (dockerversion.Version, error) { 681 | edgeDockerpath, err := getEdgeDockerPath() 682 | if err != nil { 683 | return dockerversion.Version{}, err 684 | } 685 | version, err := getDockerVersion(edgeDockerpath, true) 686 | version.SetAsEdge() 687 | return version, err 688 | } 689 | 690 | func getDockerVersion(dockerPath string, includeBuild bool) (dockerversion.Version, error) { 691 | stdout, _ := exec.Command(dockerPath, "-v").Output() 692 | rawVersion := strings.TrimSpace(string(stdout)) 693 | 694 | writeDebug("%s -v output: %s", dockerPath, rawVersion) 695 | 696 | versionRegex := regexp.MustCompile(`^Docker version (.+), build (.+)?`) 697 | match := versionRegex.FindStringSubmatch(rawVersion) 698 | if len(match) < 2 { 699 | return dockerversion.Version{}, errors.New("Could not detect docker version.") 700 | } 701 | 702 | version := string(match[1][:]) 703 | build := string(match[2][:]) 704 | if includeBuild { 705 | version = fmt.Sprintf("%s+%s", version, build) 706 | } 707 | return dockerversion.Parse(version), nil 708 | } 709 | 710 | func listRemote(prefix string) { 711 | versions := getAvailableVersions(prefix, opts.IncludePrereleases) 712 | for _, version := range versions { 713 | writeInfo(version.String()) 714 | } 715 | } 716 | 717 | func getInstalledVersions(pattern string) []dockerversion.Version { 718 | searchPath := filepath.Join(getVersionsDir(), pattern) 719 | versions, _ := filepath.Glob(searchPath) 720 | 721 | var results []dockerversion.Version 722 | for _, versionDir := range versions { 723 | version := dockerversion.Parse(filepath.Base(versionDir)) 724 | 725 | if version.IsEdge() { 726 | edgeDockerPath := filepath.Join(versionDir, getBinaryName()) 727 | edgeVersion, err := getDockerVersion(edgeDockerPath, true) 728 | if err != nil { 729 | writeDebug("Unable to get version of installed edge version at %s.\n%s", versionDir, err) 730 | continue 731 | } 732 | version = dockerversion.NewAlias(dockerversion.EdgeAlias, edgeVersion.Value()) 733 | } 734 | 735 | results = append(results, version) 736 | } 737 | 738 | if glob.Glob(pattern, dockerversion.SystemAlias) { 739 | systemVersion, err := getSystemDockerVersion() 740 | if err == nil { 741 | results = append(results, systemVersion) 742 | } 743 | } 744 | 745 | dockerversion.Sort(results) 746 | return results 747 | } 748 | 749 | func getAvailableVersions(pattern string, includePrereleases bool) []dockerversion.Version { 750 | versions := make(map[string]dockerversion.Version) 751 | 752 | writeDebug("Retrieving legacy Docker releases") 753 | legacyVersions, err := listLegacyDockerVersions() 754 | if err != nil { 755 | die("", err, retCodeRuntimeError) 756 | } 757 | for _, v := range legacyVersions { 758 | if !includePrereleases && v.IsPrerelease() { 759 | continue 760 | } 761 | if strings.HasPrefix(v.Value(), pattern) { 762 | versions[v.String()] = v 763 | } 764 | } 765 | 766 | writeDebug("Retrieving Docker releases") 767 | stableVersions, err := dockerversion.ListVersions(opts.MirrorURL, dockerversion.Stable) 768 | if err != nil { 769 | die("", err, retCodeRuntimeError) 770 | } 771 | for _, v := range stableVersions { 772 | if strings.HasPrefix(v.Value(), pattern) { 773 | versions[v.String()] = v 774 | } 775 | } 776 | 777 | if includePrereleases { 778 | writeDebug("Retrieving Docker pre-releases") 779 | prereleaseVersions, err := dockerversion.ListVersions(opts.MirrorURL, dockerversion.Test) 780 | if err != nil { 781 | die("", err, retCodeRuntimeError) 782 | } 783 | for _, v := range prereleaseVersions { 784 | if strings.HasPrefix(v.Value(), pattern) { 785 | versions[v.String()] = v 786 | } 787 | } 788 | } 789 | 790 | results := make([]dockerversion.Version, 0, len(versions)) 791 | for _, v := range versions { 792 | results = append(results, v) 793 | } 794 | 795 | dockerversion.Sort(results) 796 | 797 | return results 798 | } 799 | 800 | func listLegacyDockerVersions() ([]dockerversion.Version, error) { 801 | gh := buildGithubClient() 802 | options := &github.ListOptions{PerPage: 100} 803 | 804 | var allReleases []github.RepositoryRelease 805 | for { 806 | releases, response, err := gh.Repositories.ListReleases("moby", "moby", options) 807 | if err != nil { 808 | warnWhenRateLimitExceeded(err, response) 809 | return nil, errors.Wrap(err, "Unable to retrieve list of Docker releases from GitHub") 810 | } 811 | allReleases = append(allReleases, releases...) 812 | if response.StatusCode != 200 { 813 | return nil, errors.Errorf("Unable to retrieve list of Docker releases from GitHub (Status %v).", response.StatusCode) 814 | } 815 | if response.NextPage == 0 { 816 | break 817 | } 818 | options.Page = response.NextPage 819 | } 820 | 821 | var results []dockerversion.Version 822 | for _, release := range allReleases { 823 | version := *release.Name 824 | v := dockerversion.Parse(version) 825 | if v.IsEmpty() { 826 | writeDebug("Ignoring non-semver Docker release: %s", version) 827 | continue 828 | } 829 | results = append(results, v) 830 | } 831 | 832 | return results, nil 833 | } 834 | 835 | func isUpgradeAvailable() (bool, string) { 836 | gh := buildGithubClient() 837 | release, response, err := gh.Repositories.GetLatestRelease("howtowhale", "dvm") 838 | if err != nil { 839 | warnWhenRateLimitExceeded(err, response) 840 | writeWarning("Unable to query the latest dvm release from GitHub:") 841 | writeWarning("%s", err) 842 | return false, "" 843 | } 844 | if response.StatusCode != 200 { 845 | writeWarning("Unable to query the latest dvm release from GitHub (Status %s):", response.StatusCode) 846 | return false, "" 847 | } 848 | 849 | currentVersion, err := semver.NewVersion(dvmVersion) 850 | if err != nil { 851 | writeWarning("Unable to parse the current dvm version as a semantic version!") 852 | writeWarning("%s", err) 853 | return false, "" 854 | } 855 | latestVersion, err := semver.NewVersion(*release.TagName) 856 | if err != nil { 857 | writeWarning("Unable to parse the latest dvm version as a semantic version!") 858 | writeWarning("%s", err) 859 | return false, "" 860 | } 861 | 862 | return latestVersion.GreaterThan(currentVersion), *release.TagName 863 | } 864 | 865 | func getVersionsDir() string { 866 | return filepath.Join(opts.DvmDir, "bin", "docker") 867 | } 868 | 869 | func getVersionDir(version dockerversion.Version) string { 870 | versionPath := version.Slug() 871 | if version.IsEdge() { 872 | versionPath = dockerversion.EdgeAlias 873 | } 874 | return filepath.Join(getVersionsDir(), versionPath) 875 | } 876 | 877 | func getDockerVersionVar() string { 878 | return strings.TrimSpace(os.Getenv("DOCKER_VERSION")) 879 | } 880 | 881 | func buildGithubClient() *github.Client { 882 | if opts.Token != "" { 883 | tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: opts.Token}) 884 | httpClient := oauth2.NewClient(oauth2.NoContext, tokenSource) 885 | return github.NewClient(httpClient) 886 | } 887 | 888 | client := github.NewClient(nil) 889 | if githubUrlOverride != "" { 890 | var err error 891 | client.BaseURL, err = neturl.Parse(githubUrlOverride) 892 | if err != nil { 893 | die("Invalid github url override: %s", err, retCodeInvalidArgument, githubUrlOverride) 894 | } 895 | } 896 | return client 897 | } 898 | 899 | func warnWhenRateLimitExceeded(err error, response *github.Response) { 900 | if err == nil { 901 | return 902 | } 903 | 904 | if response != nil { 905 | if response.StatusCode == 403 { 906 | writeWarning("Your GitHub API rate limit has been exceeded. Set the GITHUB_TOKEN environment variable or use the --github-token parameter with your GitHub personal access token to authenticate and increase the rate limit.") 907 | } 908 | } 909 | } 910 | -------------------------------------------------------------------------------- /dvm-helper/dvm-helper.linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package main 5 | 6 | const dvmOS string = "Linux" 7 | -------------------------------------------------------------------------------- /dvm-helper/dvm-helper.nix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package main 5 | 6 | import ( 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/howtowhale/dvm/dvm-helper/internal/downloader" 11 | ) 12 | 13 | const binaryFileExt string = "" 14 | 15 | func upgradeSelf(version string) { 16 | d := downloader.New(opts) 17 | 18 | binaryURL := buildDvmReleaseURL(version, dvmOS, dvmArch, "dvm-helper") 19 | binaryPath := filepath.Join(opts.DvmDir, "dvm-helper", "dvm-helper") 20 | err := d.DownloadFileWithChecksum(binaryURL, binaryPath) 21 | if err != nil { 22 | die("", err, retCodeRuntimeError) 23 | } 24 | 25 | scriptURL := buildDvmReleaseURL(version, "dvm.sh") 26 | scriptPath := filepath.Join(opts.DvmDir, "dvm.sh") 27 | err = d.DownloadFile(scriptURL, scriptPath) 28 | if err != nil { 29 | die("", err, retCodeRuntimeError) 30 | } 31 | } 32 | 33 | func getCleanPathRegex() string { 34 | versionDir := getVersionsDir() 35 | return versionDir + `/[^:]+:` 36 | } 37 | 38 | func validateShellFlag() { 39 | // we don't care about the shell flag on non-Windows platforms 40 | } 41 | 42 | func getUserHomeDir() string { 43 | return os.Getenv("HOME") 44 | } 45 | -------------------------------------------------------------------------------- /dvm-helper/dvm-helper.windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | 12 | "github.com/howtowhale/dvm/dvm-helper/internal/downloader" 13 | ) 14 | 15 | const dvmOS string = "Windows" 16 | const binaryFileExt string = ".exe" 17 | 18 | func upgradeSelf(version string) { 19 | d := downloader.New(opts) 20 | 21 | binaryURL := buildDvmReleaseURL(version, dvmOS, dvmArch, "dvm-helper.exe") 22 | binaryPath := filepath.Join(opts.DvmDir, ".tmp", "dvm-helper.exe") 23 | err := d.DownloadFileWithChecksum(binaryURL, binaryPath) 24 | if err != nil { 25 | die("", err, retCodeRuntimeError) 26 | } 27 | 28 | psScriptURL := buildDvmReleaseURL(version, "dvm.ps1") 29 | psScriptPath := filepath.Join(opts.DvmDir, "dvm.ps1") 30 | err = d.DownloadFile(psScriptURL, psScriptPath) 31 | if err != nil { 32 | die("", err, retCodeRuntimeError) 33 | } 34 | 35 | cmdScriptURL := buildDvmReleaseURL(version, "dvm.cmd") 36 | cmdScriptPath := filepath.Join(opts.DvmDir, "dvm.cmd") 37 | err = d.DownloadFile(cmdScriptURL, cmdScriptPath) 38 | if err != nil { 39 | die("", err, retCodeRuntimeError) 40 | } 41 | 42 | writeUpgradeScript() 43 | } 44 | 45 | func writeUpgradeScript() { 46 | scriptPath := buildDvmOutputScriptPath() 47 | tmpBinaryPath := filepath.Join(opts.DvmDir, ".tmp", "dvm-helper.exe") 48 | binaryPath := filepath.Join(opts.DvmDir, "dvm-helper", "dvm-helper.exe") 49 | 50 | var contents string 51 | if opts.Shell == "powershell" { 52 | contents = fmt.Sprintf("cp -force '%s' '%s'", tmpBinaryPath, binaryPath) 53 | } else { // cmd 54 | contents = fmt.Sprintf("cp /Y '%s' '%s'", tmpBinaryPath, binaryPath) 55 | } 56 | 57 | writeFile(scriptPath, contents) 58 | } 59 | 60 | func getCleanPathRegex() string { 61 | versionDir := getVersionsDir() 62 | escapedVersionDir := strings.Replace(versionDir, `\`, `\\`, -1) 63 | return escapedVersionDir + `\\[^:]+;` 64 | } 65 | 66 | func validateShellFlag() { 67 | if opts.Shell != "powershell" && opts.Shell != "cmd" { 68 | die("The --shell flag or SHELL environment variable must be set when running on Windows. Available values are powershell and cmd.", nil, retCodeInvalidArgument) 69 | } 70 | } 71 | 72 | func getUserHomeDir() string { 73 | return os.Getenv("USERPROFILE") 74 | } 75 | -------------------------------------------------------------------------------- /dvm-helper/dvm-helper_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "os" 9 | "testing" 10 | 11 | "github.com/fatih/color" 12 | "github.com/howtowhale/dvm/dvm-helper/internal/test" 13 | "github.com/ryanuber/go-glob" 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | type requestHandler func(w http.ResponseWriter, r *http.Request) 18 | 19 | func docker1_10_3_Handler(w http.ResponseWriter, r *http.Request) { 20 | w.Header().Set("Content-Type", "application/json") 21 | 22 | switch { 23 | case glob.Glob("*/version", r.RequestURI): 24 | fmt.Fprintln(w, `{ 25 | "Version": "swarm/1.2.3", 26 | "ApiVersion": "1.22" 27 | }`) 28 | default: 29 | w.WriteHeader(404) 30 | } 31 | } 32 | 33 | func docker1_12_1_Handler(w http.ResponseWriter, r *http.Request) { 34 | w.Header().Set("Content-Type", "application/json") 35 | 36 | switch { 37 | case glob.Glob("*/version", r.RequestURI): 38 | fmt.Fprintln(w, `{ 39 | "Version": "1.12.1" 40 | }`) 41 | default: 42 | w.WriteHeader(404) 43 | } 44 | } 45 | 46 | func githubReleasesHandler(w http.ResponseWriter, r *http.Request) { 47 | w.Header().Set("Content-Type", "application/json") 48 | 49 | switch r.RequestURI { 50 | case "/repos/moby/moby/releases?per_page=100": 51 | fmt.Fprintln(w, test.LoadTestData("github-docker-releases.json")) 52 | default: 53 | w.WriteHeader(404) 54 | } 55 | } 56 | 57 | func createMockDVM(dockerHandler requestHandler) (docker *httptest.Server, github *httptest.Server) { 58 | github = httptest.NewServer(http.HandlerFunc(githubReleasesHandler)) 59 | githubUrlOverride = github.URL + "/" 60 | 61 | if dockerHandler != nil { 62 | docker = httptest.NewServer(http.HandlerFunc(dockerHandler)) 63 | } 64 | return 65 | } 66 | 67 | func TestDetectOldVersion(t *testing.T) { 68 | docker, github := createMockDVM(docker1_10_3_Handler) 69 | defer docker.Close() 70 | defer github.Close() 71 | 72 | os.Setenv("DOCKER_HOST", docker.URL) 73 | os.Setenv("DOCKER_TLS_VERIFY", "0") 74 | os.Unsetenv("DOCKER_CERT_PATH") 75 | 76 | outputCapture := &bytes.Buffer{} 77 | color.Output = outputCapture 78 | 79 | dvm := makeCliApp() 80 | dvm.Run([]string{"dvm", "--debug", "detect"}) 81 | 82 | version := os.Getenv("DOCKER_VERSION") 83 | assert.Equal(t, version, "1.10.3", "Detected the wrong version") 84 | 85 | output := outputCapture.String() 86 | assert.NotEmpty(t, output, "Should have captured stdout") 87 | assert.Contains(t, output, "Detected client version: 1.10.3", "Should have printed the detected version") 88 | } 89 | 90 | func TestDetectVersion(t *testing.T) { 91 | docker, github := createMockDVM(docker1_12_1_Handler) 92 | defer docker.Close() 93 | defer github.Close() 94 | 95 | outputCapture := &bytes.Buffer{} 96 | color.Output = outputCapture 97 | 98 | os.Setenv("DOCKER_HOST", docker.URL) 99 | os.Setenv("DOCKER_TLS_VERIFY", "0") 100 | os.Unsetenv("DOCKER_CERT_PATH") 101 | 102 | dvm := makeCliApp() 103 | dvm.Run([]string{"dvm", "--debug", "detect"}) 104 | 105 | version := os.Getenv("DOCKER_VERSION") 106 | assert.Equal(t, version, "1.12.1", "Detected the wrong version") 107 | 108 | output := outputCapture.String() 109 | assert.NotEmpty(t, output, "Should have captured stdout") 110 | assert.Contains(t, output, "Detected client version: 1.12.1", "Should have printed the detected version") 111 | } 112 | 113 | func TestListRemote(t *testing.T) { 114 | _, github := createMockDVM(nil) 115 | defer github.Close() 116 | 117 | outputCapture := &bytes.Buffer{} 118 | color.Output = outputCapture 119 | 120 | dvm := makeCliApp() 121 | dvm.Run([]string{"dvm", "--debug", "list-remote"}) 122 | 123 | output := outputCapture.String() 124 | assert.NotEmpty(t, output, "Should have captured stdout") 125 | 126 | assert.Contains(t, output, "1.12.5", "Should have listed a legacy stable version") 127 | assert.NotContains(t, output, "1.12.5-rc1", "Should not have listed a legacy prerelease version") 128 | 129 | assert.Contains(t, output, "17.09.0-ce", "Should have listed a stable version") 130 | assert.NotContains(t, output, "17.10.0-ce-rc1", "Should not have listed a prerelease version") 131 | } 132 | 133 | func TestListRemoteWithPrereleases(t *testing.T) { 134 | _, github := createMockDVM(nil) 135 | defer github.Close() 136 | 137 | outputCapture := &bytes.Buffer{} 138 | color.Output = outputCapture 139 | 140 | dvm := makeCliApp() 141 | dvm.Run([]string{"dvm-helper", "--debug", "list-remote", "--pre"}) 142 | 143 | output := outputCapture.String() 144 | assert.NotEmpty(t, output, "Should have captured stdout") 145 | 146 | assert.Contains(t, output, "1.12.5-rc1", "Should have listed a legacy prerelease version") 147 | assert.Contains(t, output, "17.10.0-ce-rc1", "Should have listed a prerelease version") 148 | } 149 | 150 | func TestInstallPrereleases(t *testing.T) { 151 | _, github := createMockDVM(nil) 152 | defer github.Close() 153 | 154 | outputCapture := &bytes.Buffer{} 155 | color.Output = outputCapture 156 | 157 | dvm := makeCliApp() 158 | dvm.Run([]string{"dvm-helper", "--debug", "install", "18.06.1-ce"}) 159 | 160 | output := outputCapture.String() 161 | assert.NotEmpty(t, output, "Should have captured stdout") 162 | assert.Contains(t, output, "Now using Docker 18.06.1-ce", "Should have installed a prerelease version") 163 | } 164 | 165 | // install a version from the test location that is missing the -rc suffix 166 | func TestInstallNonPrereleaseTestRelease(t *testing.T) { 167 | _, github := createMockDVM(nil) 168 | defer github.Close() 169 | 170 | outputCapture := &bytes.Buffer{} 171 | color.Output = outputCapture 172 | 173 | dvm := makeCliApp() 174 | dvm.Run([]string{"dvm-helper", "--debug", "install", "17.10.0-ce"}) 175 | 176 | output := outputCapture.String() 177 | assert.NotEmpty(t, output, "Should have captured stdout") 178 | assert.Contains(t, output, "Now using Docker 17.10.0-ce", "Should have installed a test version") 179 | } 180 | 181 | // install something that used to be a test release and is now considered stable 182 | func TestInstallStabilizedTestRelease(t *testing.T) { 183 | _, github := createMockDVM(nil) 184 | defer github.Close() 185 | 186 | outputCapture := &bytes.Buffer{} 187 | color.Output = outputCapture 188 | 189 | dvm := makeCliApp() 190 | dvm.Run([]string{"dvm-helper", "--debug", "install", "17.09.0-ce"}) 191 | 192 | output := outputCapture.String() 193 | assert.NotEmpty(t, output, "Should have captured stdout") 194 | assert.Contains(t, output, "Now using Docker 17.09.0-ce", "Should have installed a stable version") 195 | } 196 | -------------------------------------------------------------------------------- /dvm-helper/internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | ) 7 | 8 | type DvmOptions struct { 9 | DvmDir string 10 | MirrorURL string 11 | Token string 12 | Shell string 13 | Debug bool 14 | Silent bool 15 | IncludePrereleases bool 16 | Logger *log.Logger 17 | } 18 | 19 | func NewDvmOptions() DvmOptions { 20 | return DvmOptions{ 21 | Logger: log.New(ioutil.Discard, "", log.LstdFlags), 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /dvm-helper/internal/downloader/downloader.go: -------------------------------------------------------------------------------- 1 | package downloader 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/http" 7 | "os" 8 | "path" 9 | "path/filepath" 10 | "strings" 11 | 12 | "github.com/howtowhale/dvm/dvm-helper/checksum" 13 | "github.com/howtowhale/dvm/dvm-helper/internal/config" 14 | "github.com/pivotal-golang/archiver/extractor" 15 | "github.com/pkg/errors" 16 | ) 17 | 18 | // Client is capable of downloading archived and checksumed files. 19 | type Client struct { 20 | log *log.Logger 21 | tmp string 22 | } 23 | 24 | // New creates a downloader client. 25 | // l - optional logger for debug output 26 | func New(opts config.DvmOptions) Client { 27 | return Client{ 28 | log: opts.Logger, 29 | tmp: filepath.Join(opts.DvmDir, ".tmp"), 30 | } 31 | } 32 | 33 | func (d Client) ensureParentDirectoryExists(path string) error { 34 | err := os.MkdirAll(filepath.Dir(path), os.ModePerm) 35 | return errors.Wrapf(err, "Unable to create parent directory %s", path) 36 | } 37 | 38 | // DownloadFile saves a file without any additional processing. 39 | func (d Client) DownloadFile(url string, destPath string) error { 40 | err := d.ensureParentDirectoryExists(destPath) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | destFile, err := os.Create(destPath) 46 | if err != nil { 47 | return errors.Wrapf(err, "Unable to create %s", destPath) 48 | } 49 | defer destFile.Close() 50 | os.Chmod(destPath, 0755) 51 | 52 | d.log.Printf("Downloading %s to %s\n", url, destPath) 53 | 54 | response, err := http.Get(url) 55 | if err != nil { 56 | return errors.Wrapf(err, "Unable to download %s", url) 57 | } 58 | 59 | if response.StatusCode != 200 { 60 | return errors.Wrapf(err, "Unable to download %s (Status %d)", url, response.StatusCode) 61 | } 62 | defer response.Body.Close() 63 | 64 | _, err = io.Copy(destFile, response.Body) 65 | return errors.Wrapf(err, "Unable to write to %s", destPath) 66 | } 67 | 68 | // Download file saves a file after verifying the checksum found at url + ".sh256". 69 | func (d Client) DownloadFileWithChecksum(url string, destPath string) error { 70 | fileName := filepath.Base(destPath) 71 | tmpPath := filepath.Join(d.tmp, fileName) 72 | err := d.DownloadFile(url, tmpPath) 73 | if err != nil { 74 | return err 75 | } 76 | 77 | checksumURL := url + ".sha256" 78 | checksumPath := filepath.Join(d.tmp, fileName+".sha256") 79 | err = d.DownloadFile(checksumURL, checksumPath) 80 | if err != nil { 81 | return err 82 | } 83 | 84 | isValid, err := checksum.CompareChecksum(tmpPath, checksumPath) 85 | if err != nil { 86 | return errors.Wrapf(err, "Unable to calculate checksum of %s", tmpPath) 87 | } 88 | if !isValid { 89 | return errors.Wrapf(err, "The checksum of %s failed to match %s", tmpPath, checksumPath) 90 | } 91 | 92 | // Copy to final location, if different 93 | if destPath != tmpPath { 94 | err = d.ensureParentDirectoryExists(destPath) 95 | if err != nil { 96 | return err 97 | } 98 | 99 | err = os.Rename(tmpPath, destPath) 100 | if err != nil { 101 | return errors.Wrapf(err, "Unable to copy %s to %s", tmpPath, destPath) 102 | } 103 | } 104 | 105 | // Cleanup temp files 106 | if err = os.Remove(checksumPath); err != nil { 107 | d.log.Println(errors.Wrapf(err, "Unable to remove temporary file %s", checksumPath)) 108 | } 109 | 110 | return nil 111 | } 112 | 113 | // DownloadArchivedFile downloads the archive, decompresses it and saves the specified file to the destination path. 114 | // url - URL of the archived file, e.g. a gzip, zip or tar file 115 | // archivedFile - relative path to the desired file in the archive 116 | // destPath - location where the archivedFile should be saved 117 | func (d Client) DownloadArchivedFile(url string, archivedFile string, destPath string) error { 118 | archiveName := path.Base(url) 119 | tmpPath := filepath.Join(d.tmp, archiveName) 120 | 121 | err := d.DownloadFile(url, tmpPath) 122 | if err != nil { 123 | return err 124 | } 125 | 126 | return d.extractArchive(tmpPath, archiveName, archivedFile, destPath) 127 | } 128 | 129 | // DownloadArchivedFileWithChecksum first verifies the checksum found at url + ".sh256", 130 | // decompresses the archive, and then saves the specified file to the destination path. 131 | // url - URL of the archived file, e.g. a gzip, zip or tar file 132 | // archivedFile - relative path to the desired file in the archive 133 | // destPath - location where the archivedFile should be saved 134 | func (d Client) DownloadArchivedFileWithChecksum(url string, archivedFile string, destPath string) error { 135 | archiveName := path.Base(url) 136 | tmpPath := filepath.Join(d.tmp, archiveName) 137 | 138 | err := d.DownloadFileWithChecksum(url, tmpPath) 139 | if err != nil { 140 | return err 141 | } 142 | 143 | return d.extractArchive(tmpPath, archiveName, archivedFile, destPath) 144 | } 145 | 146 | func (d Client) extractArchive(tmpPath string, archiveName string, archivedFile string, destPath string) error { 147 | // Extract the archive 148 | archivePath := filepath.Join(d.tmp, strings.TrimSuffix(archiveName, filepath.Ext(archiveName))) 149 | x := extractor.NewDetectable() 150 | x.Extract(tmpPath, archivePath) 151 | 152 | // Copy the archived file to the final destination 153 | archivedFilePath := filepath.Join(archivePath, archivedFile) 154 | 155 | err := d.ensureParentDirectoryExists(destPath) 156 | if err != nil { 157 | return err 158 | } 159 | 160 | err = os.Rename(archivedFilePath, destPath) 161 | if err != nil { 162 | return errors.Wrapf(err, "Unable to copy %s to %s", archivedFilePath, destPath) 163 | } 164 | 165 | // Cleanup temp files 166 | if err = os.Remove(tmpPath); err != nil { 167 | d.log.Println(errors.Wrapf(err, "Unable to remove temporary file %s", tmpPath)) 168 | } 169 | if err = os.RemoveAll(archivePath); err != nil { 170 | d.log.Println(errors.Wrapf(err, "Unable to remove temporary directory %s", archivePath)) 171 | } 172 | 173 | return nil 174 | } 175 | -------------------------------------------------------------------------------- /dvm-helper/internal/test/testdata.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | // LoadTestData reads the relative path under the testdata directory 10 | // and returns the contents. 11 | func LoadTestData(src string) string { 12 | pwd, err := os.Getwd() 13 | if err != nil { 14 | panic(err) 15 | } 16 | 17 | testFile := filepath.Join(pwd, "testdata", src) 18 | content, err := ioutil.ReadFile(testFile) 19 | if err != nil { 20 | panic(err) 21 | } 22 | return string(content) 23 | } 24 | -------------------------------------------------------------------------------- /dvm-helper/path.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "regexp" 7 | ) 8 | 9 | const pathEnvVar string = "PATH" 10 | 11 | // Get the PATH environment variable value 12 | func getPath() string { 13 | return os.Getenv(pathEnvVar) 14 | } 15 | 16 | // Set the PATH environment variable value 17 | func setPath(value string) { 18 | os.Setenv(pathEnvVar, value) 19 | } 20 | 21 | // Prepend the specified value to the PATH environment variable 22 | func prependPath(value string) { 23 | originalPath := getPath() 24 | newPath := fmt.Sprintf("%s%c%s", value, os.PathListSeparator, originalPath) 25 | setPath(newPath) 26 | } 27 | 28 | // Remove any values which match the specified regular expression 29 | // from the PATH environment variable 30 | func removePath(regexValue string) { 31 | regex, _ := regexp.Compile(regexValue) 32 | newPath := regex.ReplaceAllString(getPath(), "") 33 | setPath(newPath) 34 | } 35 | -------------------------------------------------------------------------------- /dvm-helper/url/url.go: -------------------------------------------------------------------------------- 1 | package url 2 | 3 | import "strings" 4 | 5 | // Join joins any number of path elements into a single path, adding a separating slash if necessary. 6 | // All empty strings are ignored. 7 | func Join(elem ...string) string { 8 | for i, e := range elem { 9 | if e != "" { 10 | return clean(strings.Join(elem[i:], "/")) 11 | } 12 | } 13 | return "" 14 | } 15 | 16 | func clean(urlPart string) string { 17 | return strings.TrimRight(urlPart, "/") 18 | } 19 | -------------------------------------------------------------------------------- /dvm-helper/util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/fatih/color" 10 | ) 11 | 12 | func exportEnvironmentVariable(name string) string { 13 | value := os.Getenv(name) 14 | 15 | if opts.Shell == "powershell" { 16 | return fmt.Sprintf("$env:%s=\"%s\"\r\n", name, value) 17 | } 18 | 19 | if opts.Shell == "cmd" { 20 | return fmt.Sprintf("%s=%s\r\n", name, value) 21 | } 22 | 23 | // default to bash 24 | return fmt.Sprintf("export %s=\"%s\"\n", name, value) 25 | } 26 | 27 | func ensureParentDirectoryExists(filePath string) { 28 | dir := filepath.Dir(filePath) 29 | 30 | err := os.MkdirAll(dir, 0777) 31 | if err != nil { 32 | die("Unable to create directory %s.", err, retCodeRuntimeError, dir) 33 | } 34 | } 35 | 36 | func writeFile(path string, contents string) { 37 | writeDebug("Writing to %s...", path) 38 | writeDebug(contents) 39 | 40 | ensureParentDirectoryExists(path) 41 | 42 | file, err := os.OpenFile(path, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0660) 43 | if err != nil { 44 | die("Unable to create %s", err, retCodeRuntimeError, path) 45 | } 46 | 47 | _, err = io.WriteString(file, contents) 48 | if err != nil { 49 | die("Unable to write to %s", err, retCodeRuntimeError, path) 50 | } 51 | 52 | file.Close() 53 | } 54 | 55 | func writeDebug(format string, a ...interface{}) { 56 | if !opts.Debug { 57 | return 58 | } 59 | 60 | color.Cyan(format, a...) 61 | } 62 | 63 | func writeInfo(format string, a ...interface{}) { 64 | if opts.Silent { 65 | return 66 | } 67 | 68 | color.White(format, a...) 69 | } 70 | 71 | func writeWarning(format string, a ...interface{}) { 72 | if opts.Silent { 73 | return 74 | } 75 | 76 | color.Yellow(format, a...) 77 | } 78 | 79 | func writeError(format string, err error, a ...interface{}) { 80 | color.Set(color.FgRed) 81 | fmt.Fprintf(os.Stderr, format+"\n", a...) 82 | if err != nil { 83 | fmt.Fprintln(os.Stderr, err) 84 | } 85 | color.Unset() 86 | } 87 | 88 | func die(format string, err error, exitCode int, a ...interface{}) { 89 | writeError(format, err, a...) 90 | os.Exit(exitCode) 91 | } 92 | -------------------------------------------------------------------------------- /dvm.cmd: -------------------------------------------------------------------------------- 1 | :: Docker Version Manager CMD Wrapper 2 | :: Implemented as a POSIX-compliant function 3 | :: To use, add this script's parent directory to your path 4 | 5 | @ECHO OFF 6 | SETLOCAL 7 | 8 | SET DVM_DIR=%~dp0 9 | 10 | IF NOT EXIST "%DVM_DIR%\dvm-helper\dvm-helper.exe" ( 11 | echo Installation corrupt: dvm-helper.exe is missing. Please reinstall dvm. 12 | EXIT /b 1 13 | ) 14 | 15 | SET DVM_OUTPUT="%DVM_DIR%\.tmp\dvm-output.cmd" 16 | 17 | IF EXIST "%DVM_OUTPUT%" ( 18 | DEL "%DVM_OUTPUT%" 19 | ) 20 | 21 | %DVM_DIR%\dvm-helper\dvm-helper.exe --shell cmd %* 22 | 23 | IF EXIST "%DVM_OUTPUT%" ( 24 | ENDLOCAL 25 | CALL "%DVM_OUTPUT%" 26 | ) 27 | -------------------------------------------------------------------------------- /dvm.ps1: -------------------------------------------------------------------------------- 1 | # Docker Version Manager PowerShell Wrapper 2 | # Implemented as a POSIX-compliant function 3 | # To use, source this script, `. dvm.ps1`, then type dvm help 4 | 5 | function dvm() { 6 | # Default dvm home to ~/.dvm if not set 7 | $dvmDir = $env:DVM_DIR 8 | if( $dvmDir -eq $null ) { 9 | $dvmDir = "$env:USERPROFILE\.dvm" 10 | } 11 | 12 | # Expect that dvm-helper is next to this script 13 | $dvmHelper = Join-Path $PSScriptRoot dvm-helper\dvm-helper.exe 14 | if( !(Test-Path $dvmHelper) ) { 15 | $host.ui.WriteErrorLine("Installation corrupt: dvm-helper.exe is missing. Please reinstall dvm.") 16 | return 1 17 | } 18 | 19 | # Pass dvm-helper output back to script via ~/.dvm/.tmp/dvm-output.ps1 20 | $dvmOutput = Join-Path $dvmDir .tmp\dvm-output.ps1 21 | if( Test-Path $dvmOutput ) { 22 | rm $dvmOutput 23 | } 24 | 25 | $rawArgs = $MyInvocation.Line.Substring(3).Trim() 26 | $dvmCall = "& '$dvmHelper' --shell powershell $rawArgs" 27 | iex $dvmCall 28 | 29 | if( Test-Path $dvmOutput ) { 30 | . $dvmOutput 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /dvm.sh: -------------------------------------------------------------------------------- 1 | # Docker Version Manager wrapper for *nix 2 | # Implemented as a POSIX-compliant function 3 | # Should work on sh, dash, bash, ksh, zsh 4 | # To use, source this file from your bash profile 5 | 6 | { # This ensures the entire script is downloaded 7 | 8 | DVM_SCRIPT_SOURCE="$_" 9 | 10 | __dvm_has() { 11 | type "$1" > /dev/null 2>&1 12 | } 13 | 14 | # Make zsh glob matching behave same as bash 15 | # This fixes the "zsh: no matches found" errors 16 | if __dvm_has "unsetopt"; then 17 | unsetopt nomatch 2>/dev/null 18 | DVM_CD_FLAGS="-q" 19 | fi 20 | 21 | # Default DVM_DIR to $HOME/.dvm when not set 22 | if [ -z "$DVM_DIR" ]; then 23 | DVM_DIR="$HOME/.dvm" 24 | fi 25 | 26 | # Expect that dvm-helper is next to this script 27 | if [ -n "$BASH_SOURCE" ]; then 28 | DVM_SCRIPT_SOURCE="${BASH_SOURCE[0]}" 29 | fi 30 | if command -v builtin >/dev/null 2>&1; then 31 | export DVM_HELPER="$(builtin cd $DVM_CD_FLAGS "$(dirname "${DVM_SCRIPT_SOURCE:-$0}")" > /dev/null && command pwd)/dvm-helper/dvm-helper" 32 | else 33 | export DVM_HELPER="$(cd $DVM_CD_FLAGS "$(dirname "${DVM_SCRIPT_SOURCE:-$0}")" > /dev/null && command pwd)/dvm-helper/dvm-helper" 34 | fi 35 | unset DVM_SCRIPT_SOURCE 2> /dev/null 36 | 37 | dvm() { 38 | if [ ! -f "$DVM_HELPER" ]; then 39 | echo "Installation corrupt: dvm-helper is missing. Please reinstall dvm." 40 | return 1 41 | fi 42 | 43 | # Pass dvm-helper output back to script via ~/.dvm/.tmp/dvm-output.sh 44 | DVM_OUTPUT="$DVM_DIR/.tmp/dvm-output.sh" 45 | command rm -f "$DVM_OUTPUT" 46 | 47 | $DVM_HELPER --shell sh $@ 48 | 49 | # Execute any dvm-helper output 50 | if [ -e $DVM_OUTPUT ]; then 51 | source $DVM_OUTPUT 52 | fi 53 | } 54 | 55 | # Make the dvm function available to other scripts 56 | if [ -n "$ZSH_NAME" ]; then 57 | autoload dvm 58 | else 59 | export -f dvm 60 | fi 61 | } 62 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/howtowhale/dvm 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/Masterminds/semver v0.0.0-20170707023526-c2e7f6c2f49a 7 | github.com/codegangsta/cli v1.19.1 8 | github.com/docker/docker v1.13.1 9 | github.com/fatih/color v1.5.0 10 | github.com/google/go-github v0.0.0-20160619221136-1c08387e4c91 11 | github.com/pivotal-golang/archiver v0.0.0-20170206191640-59f15fd17404 12 | github.com/pkg/errors v0.8.1-0.20161029093637-248dadf4e906 13 | github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735 14 | github.com/stretchr/testify v1.1.4 15 | golang.org/x/oauth2 v0.0.0-20170214231824-b9780ec78894 16 | ) 17 | 18 | require ( 19 | code.cloudfoundry.org/archiver v0.0.0-20200131002800-4ca7245c29b1 // indirect 20 | github.com/Microsoft/go-winio v0.3.8 // indirect 21 | github.com/Sirupsen/logrus v0.11.3-0.20170215164324-7f4b1adc7917 // indirect 22 | github.com/cyphar/filepath-securejoin v0.2.2 // indirect 23 | github.com/davecgh/go-spew v1.0.1-0.20160907170601-6d212800a42e // indirect 24 | github.com/docker/distribution v2.6.0-rc.1.0.20170216011216-4f87c800734f+incompatible // indirect 25 | github.com/docker/go-connections v0.2.2-0.20170203235624-7da10c8c50ca // indirect 26 | github.com/docker/go-units v0.3.2 // indirect 27 | github.com/golang/protobuf v1.4.2 // indirect 28 | github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 // indirect 29 | github.com/mattn/go-colorable v0.0.8-0.20170210172801-5411d3eea597 // indirect 30 | github.com/mattn/go-isatty v0.0.12 // indirect 31 | github.com/onsi/ginkgo v1.15.0 // indirect 32 | github.com/onsi/gomega v1.10.5 // indirect 33 | github.com/opencontainers/go-digest v1.0.0-rc0 // indirect 34 | github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0 // indirect 35 | golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb // indirect 36 | golang.org/x/sys v0.0.0-20210112080510-489259a85091 // indirect 37 | google.golang.org/appengine v1.0.1-0.20170206203024-2e4a801b39fc // indirect 38 | google.golang.org/protobuf v1.23.0 // indirect 39 | ) 40 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | code.cloudfoundry.org/archiver v0.0.0-20200131002800-4ca7245c29b1 h1:yd4N3o8xBCMu3z6hyIMH9SoPATjRyovVUNdefRZBsws= 2 | code.cloudfoundry.org/archiver v0.0.0-20200131002800-4ca7245c29b1/go.mod h1:EU/KcUvh4qBVN6ffu/b8t9x7SurDot+AO0HqEZ+9QOg= 3 | github.com/Masterminds/semver v0.0.0-20170707023526-c2e7f6c2f49a h1:sfxurEC2fF6fwenKeKAlrdX831dONQwmgnSBX1R8jbM= 4 | github.com/Masterminds/semver v0.0.0-20170707023526-c2e7f6c2f49a/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= 5 | github.com/Microsoft/go-winio v0.3.8 h1:dvxbxtpTIjdAbx2OtL26p4eq0iEvys/U5yrsTJb3NZI= 6 | github.com/Microsoft/go-winio v0.3.8/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= 7 | github.com/Sirupsen/logrus v0.11.3-0.20170215164324-7f4b1adc7917 h1:DOTgSwuJxNDNHdsdWODAG9SB/0z+LsZh+DmTwSLdUns= 8 | github.com/Sirupsen/logrus v0.11.3-0.20170215164324-7f4b1adc7917/go.mod h1:rmk17hk6i8ZSAJkSDa7nOxamrG+SP4P0mm+DAvExv4U= 9 | github.com/codegangsta/cli v1.19.1 h1:+wkU9+nidApJ051CVhVGnj5li64qOfLPz7eZMn2DPXw= 10 | github.com/codegangsta/cli v1.19.1/go.mod h1:/qJNoX69yVSKu5o4jLyXAENLRyk1uhi7zkbQ3slBdOA= 11 | github.com/cyphar/filepath-securejoin v0.2.2 h1:jCwT2GTP+PY5nBz3c/YL5PAIbusElVrPujOBSCj8xRg= 12 | github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= 13 | github.com/davecgh/go-spew v1.0.1-0.20160907170601-6d212800a42e h1:9EoM2C6YAkhnxTxG3LrAos2/KaALZdSNG5HTGPEEedE= 14 | github.com/davecgh/go-spew v1.0.1-0.20160907170601-6d212800a42e/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/docker/distribution v2.6.0-rc.1.0.20170216011216-4f87c800734f+incompatible h1:g9rCimuvXuFhUcv+hOgbOq0VUCzm4WOUrSnhSgf4kWA= 16 | github.com/docker/distribution v2.6.0-rc.1.0.20170216011216-4f87c800734f+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= 17 | github.com/docker/docker v1.13.1 h1:IkZjBSIc8hBjLpqeAbeE5mca5mNgeatLHBy3GO78BWo= 18 | github.com/docker/docker v1.13.1/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 19 | github.com/docker/go-connections v0.2.2-0.20170203235624-7da10c8c50ca h1:y8QNCXaQavmcy33wXLFEopYBHGmpUdEAmGw390KAUpQ= 20 | github.com/docker/go-connections v0.2.2-0.20170203235624-7da10c8c50ca/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= 21 | github.com/docker/go-units v0.3.2 h1:Kjm80apys7gTtfVmCvVY8gwu10uofaFSrmAKOVrtueE= 22 | github.com/docker/go-units v0.3.2/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 23 | github.com/fatih/color v1.5.0 h1:vBh+kQp8lg9XPr56u1CPrWjFXtdphMoGWVHr9/1c+A0= 24 | github.com/fatih/color v1.5.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 25 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 26 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 27 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 28 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 29 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 30 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 31 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 32 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 33 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 34 | github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= 35 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 36 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 37 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 38 | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= 39 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 40 | github.com/google/go-github v0.0.0-20160619221136-1c08387e4c91 h1:1enRYvgHBPJJNoSeAvXb13qxJylZeknckj2pvir470A= 41 | github.com/google/go-github v0.0.0-20160619221136-1c08387e4c91/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= 42 | github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 h1:zLTLjkaOFEFIOxY5BWLFLwh+cL8vOBW4XJ2aqLE/Tf0= 43 | github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 44 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 45 | github.com/mattn/go-colorable v0.0.8-0.20170210172801-5411d3eea597 h1:Zw9aNQ5CQ3SERpCiZrPh4vdWcBWogoEd5m8AFf2QWVs= 46 | github.com/mattn/go-colorable v0.0.8-0.20170210172801-5411d3eea597/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 47 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 48 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 49 | github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= 50 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 51 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 52 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 53 | github.com/onsi/ginkgo v1.15.0 h1:1V1NfVQR87RtWAgp1lv9JZJ5Jap+XFGKPi00andXGi4= 54 | github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg= 55 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 56 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 57 | github.com/onsi/gomega v1.10.5 h1:7n6FEkpFmfCoo2t+YYqXH0evK+a9ICQz0xcAy9dYcaQ= 58 | github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48= 59 | github.com/opencontainers/go-digest v1.0.0-rc0 h1:YHPGfp+qlmg7loi376Jk5jNEgjgUUIdXGFsel8aFHnA= 60 | github.com/opencontainers/go-digest v1.0.0-rc0/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= 61 | github.com/pivotal-golang/archiver v0.0.0-20170206191640-59f15fd17404 h1:yeetD1u44OGkOLoO+KuaeMvqqUlG1DUAOJ2un459sFk= 62 | github.com/pivotal-golang/archiver v0.0.0-20170206191640-59f15fd17404/go.mod h1:ar7bnlyygrresc3WEZSF4cKObxnIx2LLyLfiP7caoA0= 63 | github.com/pkg/errors v0.8.1-0.20161029093637-248dadf4e906 h1:BKfCEBHnHoXswNe0Btj/zOfiyn40z6qOti8SeLbQgdM= 64 | github.com/pkg/errors v0.8.1-0.20161029093637-248dadf4e906/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 65 | github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0 h1:GD+A8+e+wFkqje55/2fOVnZPkoDIu1VooBWfNrnY8Uo= 66 | github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 67 | github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735 h1:7YvPJVmEeFHR1Tj9sZEYsmarJEQfMVYpd/Vyy/A8dqE= 68 | github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= 69 | github.com/stretchr/testify v1.1.4 h1:ToftOQTytwshuOSj6bDSolVUa3GINfJP/fg3OkkOzQQ= 70 | github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 71 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 72 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 73 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 74 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 75 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 76 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 77 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 78 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 79 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 80 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 81 | golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb h1:eBmm0M9fYhWpKZLjQUUKka/LtIxf46G4fxeEz5KJr9U= 82 | golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 83 | golang.org/x/oauth2 v0.0.0-20170214231824-b9780ec78894 h1:0B6wMEHW7F6Hzd0UV/jBov9bd6ssDnN5pIJokDjMxh8= 84 | golang.org/x/oauth2 v0.0.0-20170214231824-b9780ec78894/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 85 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 86 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 87 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 88 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 89 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 90 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 91 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 92 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 93 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 94 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 95 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 96 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 97 | golang.org/x/sys v0.0.0-20210112080510-489259a85091 h1:DMyOG0U+gKfu8JZzg2UQe9MeaC1X+xQWlAKcRnjxjCw= 98 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 99 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 100 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 101 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 102 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 103 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 104 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 105 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 106 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 107 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 108 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 109 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 110 | google.golang.org/appengine v1.0.1-0.20170206203024-2e4a801b39fc h1:/VptPD7on9gLG0uID5zYhnk2DLAMJQeCjXZBSKdGRTE= 111 | google.golang.org/appengine v1.0.1-0.20170206203024-2e4a801b39fc/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 112 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 113 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 114 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 115 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 116 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 117 | google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= 118 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 119 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 120 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 121 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 122 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 123 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 124 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 125 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 126 | -------------------------------------------------------------------------------- /install.ps1: -------------------------------------------------------------------------------- 1 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 2 | $ErrorActionPreference = "Stop" 3 | 4 | function downloadDvm([string] $dvmDir) { 5 | $webClient = New-Object net.webclient 6 | 7 | echo "Downloading dvm.ps1..." 8 | $webClient.DownloadFile("https://howtowhale.github.io/dvm/downloads/latest/dvm.ps1", "$dvmDir\dvm.ps1") 9 | 10 | echo "Downloading dvm.cmd..." 11 | $webClient.DownloadFile("https://howtowhale.github.io/dvm/downloads/latest/dvm.cmd", "$dvmDir\dvm.cmd") 12 | 13 | echo "Downloading dvm-helper.exe..." 14 | $tmpDir = Join-Path $dvmDir .tmp 15 | if( !(Test-Path $tmpDir) ) { 16 | mkdir $tmpDir > $null 17 | } 18 | 19 | # Ensure dvm-helper directory exists 20 | $dvmHelperDir = Join-Path $dvmDir dvm-helper 21 | if( !(Test-Path $dvmHelperDir ) ) { 22 | mkdir $dvmHelperDir > $null 23 | } 24 | 25 | 26 | # Detect x86 vs. x64 27 | if( [System.Environment]::Is64BitOperatingSystem ) { $arch = "x86_64" } else { $arch = "i686"} 28 | 29 | # Download latest release 30 | $webClient.DownloadFile("https://howtowhale.github.io/dvm/downloads/latest/Windows/$arch/dvm-helper.exe", "$tmpDir\dvm-helper.exe") 31 | $webClient.DownloadFile("https://howtowhale.github.io/dvm/downloads/latest/Windows/$arch/dvm-helper.exe.sha256", "$tmpDir\dvm-helper.exe.256") 32 | 33 | # Verify the binary was downloaded successfully 34 | $checksum = (cat $tmpDir\dvm-helper.exe.256).Split(' ')[0] 35 | $hash = (Get-FileHash $tmpDir\dvm-helper.exe).Hash 36 | if([string]::Compare($checksum, $hash, $true) -ne 0) { 37 | $host.ui.WriteErrorLine("DANGER! The downloaded dvm-helper binary, $tmpDir\dvm-helper.exe, does not match its checksum!") 38 | $host.ui.WriteErrorLine("CHECKSUM: $checksum") 39 | $host.ui.WriteErrorLine("HASH: $hash") 40 | return 1 41 | } 42 | 43 | mv -force "$tmpDir\dvm-helper.exe" "$dvmDir\dvm-helper\dvm-helper.exe" 44 | } 45 | 46 | function installDvm() 47 | { 48 | $dvmDir = $env:DVM_DIR 49 | if( $dvmDir -eq $null ) { 50 | $dvmDir = "$env:USERPROFILE\.dvm" 51 | } 52 | 53 | if( !(Test-Path $dvmDir) ) { 54 | mkdir $dvmDir > $null 55 | } 56 | 57 | downloadDvm $dvmDir 58 | 59 | echo "" 60 | echo "Docker Version Manager (dvm) has been installed to $dvmDir" 61 | echo "" 62 | echo "PowerShell Users: Run the following command to start using dvm. Then add it to your PowerShell profile to complete the installation." 63 | echo "`t. '$dvmDir\dvm.ps1'" 64 | echo "" 65 | echo "CMD Users: Run the first command to start using dvm. Then run the second command to add dvm to your PATH to complete the installation." 66 | echo "`t1. PATH=%PATH%;$dvmDir" 67 | echo "`t2. setx PATH `"%PATH%;$dvmDir`"" 68 | } 69 | 70 | if($PSVersionTable -eq $null -or $PSVersionTable.PSVersion.Major -lt 4){ 71 | Write-Output "dvm requires PowerShell version 4 or higher." 72 | Exit 1 73 | } 74 | 75 | installDvm 76 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | { # this ensures the entire script is downloaded # 4 | 5 | set -e 6 | 7 | dvm_has() { 8 | type "$1" > /dev/null 2>&1 9 | } 10 | 11 | dvm_checksum() { 12 | local DVM_CHECKSUM 13 | if dvm_has "sha256sum" && ! dvm_is_alias "sha256sum"; then 14 | DVM_CHECKSUM="$(command sha256sum "$1" | command awk '{print $1}')" 15 | elif dvm_has "shasum" && ! dvm_is_alias "shasum"; then 16 | DVM_CHECKSUM="$(shasum -a 256 "$1" | command awk '{print $1}')" 17 | else 18 | echo "Unaliased sha256sum, shasum not found." >&2 19 | return 2 20 | fi 21 | 22 | if [ "_$DVM_CHECKSUM" = "_$2" ]; then 23 | return 24 | elif [ -z "$2" ]; then 25 | echo 'Checksums empty' 26 | return 27 | else 28 | echo 'Checksums do not match.' >&2 29 | return 1 30 | fi 31 | } 32 | 33 | dvm_download() { 34 | if dvm_has "curl"; then 35 | curl -q $* 36 | elif dvm_has "wget"; then 37 | # Emulate curl with wget 38 | ARGS=$(echo "$*" | command sed -e 's/--progress-bar /--progress=bar /' \ 39 | -e 's/-L //' \ 40 | -e 's/-I /--server-response /' \ 41 | -e 's/-s /-q /' \ 42 | -e 's/-o /-O /') 43 | eval wget $ARGS 44 | fi 45 | } 46 | 47 | install_dvm_helper() { 48 | local url 49 | local bin 50 | 51 | # Detect mac vs. linux and x86 vs. x64 52 | DVM_OS=$(uname -s) 53 | DVM_ARCH=$(uname -m) 54 | 55 | # Download latest release 56 | mkdir -p "$DVM_DIR/dvm-helper" 57 | bin="$DVM_DIR/dvm-helper/dvm-helper" 58 | url=https://howtowhale.github.io/dvm/downloads/latest/$DVM_OS/$DVM_ARCH/dvm-helper 59 | dvm_download -L --progress-bar $url -o "$bin" 60 | chmod u+x $bin 61 | } 62 | 63 | 64 | if [ -z "$DVM_DIR" ]; then 65 | DVM_DIR="$HOME/.dvm" 66 | fi 67 | 68 | if [ ! -d "$DVM_DIR" ]; then 69 | mkdir $DVM_DIR 70 | fi 71 | 72 | echo "Downloading dvm.sh..." 73 | dvm_download -L --progress-bar https://howtowhale.github.io/dvm/downloads/latest/dvm.sh -o $DVM_DIR/dvm.sh 74 | 75 | echo "Downloading bash_completion" 76 | dvm_download -L --progress-bar https://howtowhale.github.io/dvm/downloads/latest/bash_completion -o $DVM_DIR/bash_completion 77 | 78 | echo "Downloading dvm-helper..." 79 | install_dvm_helper 80 | 81 | echo "" 82 | echo "Docker Version Manager (dvm) has been installed to ${DVM_DIR}" 83 | echo "Run the following command to start using dvm. Then add it to your bash profile (e.g. ~/.bashrc or ~/.bash_profile) to complete the installation." 84 | echo "" 85 | echo " source ${DVM_DIR}/dvm.sh" 86 | 87 | } 88 | -------------------------------------------------------------------------------- /script/deploy-gh-pages.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | rm -fr _deploy/ &> /dev/null 5 | git clone --branch gh-pages --depth 1 git@github.com:howtowhale/dvm.git _deploy 6 | 7 | cp -R bin/dvm/$VERSION _deploy/downloads/ 8 | cp -R bin/dvm/$PERMALINK _deploy/downloads/ 9 | cd _deploy/ 10 | git add downloads/ 11 | 12 | git config user.name "Travis CI" 13 | git config user.email "travis@travis-ci.org" 14 | git commit -m "Publish $VERSION from Travis Build #${TRAVIS_BUILD_NUMBER:-1}" 15 | git push 16 | -------------------------------------------------------------------------------- /script/travis.deploy.pem.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/howtowhale/dvm/4ceba054e97cbb3a1329db1c08cc350807d5ea5a/script/travis.deploy.pem.enc --------------------------------------------------------------------------------