├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── VERSION ├── captain.go ├── captain.yml ├── captain_test.go ├── cmd └── captain │ ├── cmd.go │ └── main.go ├── colors.go ├── colors_test.go ├── config.go ├── config_test.go ├── docker.go ├── docker_test.go ├── execute.go ├── execute_test.go ├── exitstatus.go ├── git.go ├── git_test.go ├── go.mod ├── go.sum ├── install.sh ├── print.go ├── print_test.go ├── release.sh └── test ├── OneImage ├── Dockerfile └── captain.yml ├── Simple ├── Dockerfile ├── Dockerfile.backend └── captain.yml ├── alpine └── captain.yml ├── buildArgs ├── Dockerfile └── captain.yml └── noCaptainYML ├── Dockerfile └── Dockerfile.error /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | build/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.6 5 | 6 | services: 7 | - docker 8 | 9 | before_install: 10 | - go get github.com/Masterminds/glide 11 | - go get golang.org/x/tools/cmd/cover 12 | - go get github.com/mattn/goveralls 13 | - glide install 14 | - go get 15 | 16 | script: 17 | - go test -v -covermode=count -coverprofile=coverage.out 18 | - goveralls -coverprofile=coverage.out -service=travis-ci -repotoken $COVERALLS_TOKEN 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.6.2 2 | RUN mkdir -p /go/src/app 3 | WORKDIR /go/src/app 4 | 5 | ADD . /go/src/app/ 6 | 7 | RUN go-wrapper download 8 | RUN go-wrapper install 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Harbur.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Go parameters 2 | GOCMD=go 3 | 4 | build: 5 | docker build -t harbur/captain . 6 | 7 | run: 8 | docker run harbur/captain 9 | 10 | b: 11 | go install -v ./cmd/captain 12 | 13 | deps: 14 | $(GOCMD) mod download 15 | $(GOCMD) mod tidy 16 | $(GOCMD) mod vendor 17 | $(GOCMD) mod verify 18 | 19 | watch: 20 | docker run -it --rm --name captain -v "$$PWD":/go/src/github.com/harbur/captain -w /go/src/github.com/harbur/captain golang:1.4 watch -n 1 make b 21 | 22 | goconvey: 23 | goconvey -timeout 10s 24 | 25 | 26 | cross: 27 | mkdir -p build 28 | gox --os windows --os linux --os darwin --arch 386 --arch amd64 github.com/harbur/captain/cmd/captain 29 | mv captain_{darwin,linux,windows}_* build 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/harbur/captain.svg?branch=master)](https://travis-ci.org/harbur/captain) [![Coverage Status](https://coveralls.io/repos/github/harbur/captain/badge.svg?branch=master)](https://coveralls.io/github/harbur/captain?branch=master) 2 | 3 | # Introduction 4 | 5 | Captain - Convert your Git workflow to Docker containers ready for Continuous Delivery 6 | 7 | Define your workflow in the `captain.yaml` and use captain to your Continuous Delivery service to create containers for each commit, test them and push them to your registry only when tests passes. 8 | 9 | * Use `captain build` to build your Dockerfile(s) of your repository. If your repository has local changes the containers will only be tagged as *latest*, otherwise the containers will be tagged as *latest*, *COMMIT_ID* & *BRANCH_NAME*. Now your Git commit tree is reproduced in your local docker repository. 10 | * Use `captain test` to run your tests 11 | * Use `captain push` to send selected images to the remote repository 12 | 13 | From the other side, you can now pull the feature branch you want to test, or create distribution channels (such as 'alpha', 'beta', 'stable') using git tags that are propagated to container tags. 14 | 15 | ![intro](https://cloud.githubusercontent.com/assets/367397/6997822/c9aeadd8-dbcb-11e4-9901-dd62bcb33e5e.gif) 16 | 17 | ## Installation 18 | 19 | To install Captain, run: 20 | ``` 21 | curl -sSL https://raw.githubusercontent.com/harbur/captain/v1.1.3/install.sh | bash 22 | ``` 23 | 24 | You will need to add `~/.captain/bin` in your `PATH`. E.g. in your `.bashrc` or `.zshrc` add: 25 | ``` 26 | export PATH=$HOME/.captain/bin:$PATH 27 | ``` 28 | 29 | ## Captain.yml Format 30 | 31 | Captain will automatically configure itself with sane values without the need for any pre-configuration, so that it will work in most cases. When it doesn't, the `captain.yml` file can be used to configure it properly. This is a simple YAML file placed on the root directory of your git repository. Captain will look for it and use it to be configured. 32 | 33 | Here is a full `captain.yml` example: 34 | 35 | ```yaml 36 | hello-world: 37 | build: Dockerfile 38 | image: harbur/hello-world 39 | pre: 40 | - echo "Preparing hello-world" 41 | post: 42 | - echo "Finished hello-world" 43 | hello-world-test: 44 | build: Dockerfile.test 45 | image: harbur/hello-world-test 46 | pre: 47 | - echo "Preparing hello-world-test" 48 | post: 49 | - echo "Finished hello-world-test" 50 | test: 51 | - docker run -e NODE_ENV=TEST harbur/hello-world-test node mochaTest 52 | - docker run -e NODE_ENV=TEST harbur/hello-world-test node karmaTest 53 | project-with-build-args: 54 | build: Dockerfile 55 | image: harbur/buildargs 56 | build_arg: 57 | keyname: keyvalue 58 | ``` 59 | 60 | ### image 61 | 62 | The location of the Dockerfile to be compiled. 63 | 64 | When auto-detecting, the image will be re-constructed by the following rules: 65 | - `Dockerfile`: `username`/`parent_dir` 66 | - `Dockerfile.*`: `username`/`parent_dir`.`parsed_suffix` 67 | 68 | Where 69 | 70 | - `username` is the host's username 71 | - `parent_dir` is the Dockerfile's parent directory name 72 | - `parsed_suffix`: is the suffix of the Dockerfile parsed with the following rules: 73 | - Lower-cased to avoid invalid repository names (Repository names support only lowercase letters, digits and _ - . characters are allowed) 74 | 75 | ```yaml 76 | image: harbur/hello-world 77 | ``` 78 | 79 | ### build 80 | 81 | The relative path of the Dockerfile to be used to compile the image. The Dockerfile's directory is also the build context that is sent to the Docker daemon. 82 | 83 | When auto-detecting it will walk current directory and all subdirectories to locate Dockerfiles of the following format: 84 | 85 | - `Dockerfile` 86 | - `Dockerfile.*` 87 | 88 | The build path will be reconstructed automatically to compile the Dockerfile. The build context will be the directory where the Dockerfile is located. 89 | 90 | Note: If more than one Dockerfiles are needed on specific directory, suffix can be used to separate them and share the same build context. 91 | 92 | ```yaml 93 | build: Dockerfile 94 | build: Dockerfile.custom 95 | build: path/to/file/Dockerfile 96 | build: path/to/file/Dockerfile.custom 97 | ``` 98 | 99 | ### test 100 | 101 | A list of commands that are run as tests after the compilation of the specific image. If any command fail, then captain stops and reports a non-zero exit status. 102 | 103 | ```yaml 104 | test: 105 | - docker run -e NODE_ENV=TEST harbur/hello-world-test node mochaTest 106 | - docker run -e NODE_ENV=TEST harbur/hello-world-test node karmaTest 107 | ``` 108 | 109 | ### pre 110 | 111 | A list of commands that are run as preparation before the compilation of the specific image. If any command fail, then captain stops and reports a non-zero exit status. 112 | 113 | ```yaml 114 | pre: 115 | - echo "Preparing compilation" 116 | ``` 117 | 118 | ### post 119 | 120 | A list of commands that are run as post-execution after the compilation of the specific image. If any command fail, then captain stops and reports a non-zero exit status. 121 | 122 | ```yaml 123 | post: 124 | - echo "Reporting after compilation" 125 | ``` 126 | 127 | ### build_arg 128 | 129 | A set of key values that are passed to docker build as `--build-arg` flag. For more information about build-args see [here](https://docs.docker.com/engine/reference/commandline/build/). 130 | 131 | ```yaml 132 | build_arg: 133 | keyname: keyvalue 134 | ``` 135 | 136 | ## CLI Commands 137 | 138 | ### build 139 | 140 | Builds the docker image(s) of your repository 141 | 142 | It will build the docker image(s) described on captain.yml in order they appear on file 143 | 144 | Flags: 145 | 146 | ``` 147 | -B, --all-branches=false: Build all branches on specific commit instead of just working branch 148 | -f, --force=false: Force build even if image is already built 149 | ``` 150 | 151 | ### test 152 | 153 | Runs the tests 154 | 155 | It will execute the commands described on test section in order they appear on file 156 | 157 | ### push 158 | 159 | Pushes the images to remote registry 160 | 161 | It will push the generated images to the remote registry 162 | 163 | By default it pushes the 'latest' and the 'branch' docker tags. 164 | 165 | Flags: 166 | 167 | ``` 168 | -B, --all-branches=false: Push all branches on specific commit instead of just working branch 169 | -b, --branch-tags=true: Push the 'branch' docker tags 170 | -c, --commit-tags=false: Push the 'commit' docker tags 171 | ``` 172 | 173 | ### pull 174 | 175 | Pulls the images from remote registry 176 | 177 | It will pull the images from the remote registry 178 | 179 | By default it pulls the 'latest' and the 'branch' docker tags. 180 | 181 | Flags: 182 | 183 | ``` 184 | -B, --all-branches=false: Pull all branches on specific commit instead of just working branch 185 | -b, --branch-tags=true: Pull the 'branch' docker tags 186 | -c, --commit-tags=false: Pull the 'commit' docker tags 187 | ``` 188 | 189 | ### self-update 190 | 191 | Updates Captain to the last available version. 192 | 193 | ### version 194 | 195 | Display version 196 | 197 | Displays the version of Captain 198 | 199 | ### help 200 | 201 | Help provides help for any command in the application. 202 | 203 | Simply type `captain help [path to command]` for full details. 204 | 205 | ## Global CLI Flags 206 | 207 | ``` 208 | -D, --debug=false: Enable debug mode 209 | -h, --help=false: help for captain 210 | -N, --namespace="username": Set default image namespace 211 | ``` 212 | 213 | ## Docker Tags Lifecycle 214 | 215 | The following is the workflow of tagging Docker images according to git state. 216 | 217 | - If you're in non-git repository, captain will tag the built images with `latest`. 218 | - If you're in dirty-git repository, captain will tag the built images with `latest`. 219 | - If you're in pristine-git repository, captain will tag the built images with `latest`, `commit-id`, `branch-name`, `tag-name`. A maximum of one tag per commit id is supported. 220 | 221 | ## Roadmap 222 | 223 | Here are some of the features pending to be implemented: 224 | 225 | * Environment variables to set captain flags 226 | * Implementation of `captain detect` that outputs the generated `captain.yml` with auto-detected content. 227 | * Implementation of `captain ci [travis|circle|etc.]` to output configuration wrappers for each CI service 228 | * Configure which images are to be pushed (e.g. to exclude test images) 229 | * Configure which tag regex are to be pushed (e.g. to exclude development sandbox branches) 230 | 231 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | v1.1.3 -------------------------------------------------------------------------------- /captain.go: -------------------------------------------------------------------------------- 1 | package captain // import "github.com/harbur/captain" 2 | 3 | import ( 4 | "fmt" 5 | "gopkg.in/cheggaaa/pb.v1" 6 | "io" 7 | "io/ioutil" 8 | "net/http" 9 | "os" 10 | "path/filepath" 11 | "runtime" 12 | ) 13 | 14 | // Debug can be turned on to enable debug mode. 15 | var Debug bool 16 | 17 | // StatusError provides error code and id 18 | type StatusError struct { 19 | error error 20 | status int 21 | } 22 | 23 | // Pre function executes commands on pre section before build 24 | func Pre(app App) error { 25 | for _, value := range app.Pre { 26 | info("Running pre command: %s", value) 27 | res := execute("bash", "-c", value) 28 | if res != nil { 29 | return res 30 | } 31 | } 32 | return nil 33 | } 34 | 35 | // Post function executes commands on pre section after build 36 | func Post(app App) error { 37 | for _, value := range app.Post { 38 | info("Running post command: %s", value) 39 | res := execute("bash", "-c", value) 40 | if res != nil { 41 | return res 42 | } 43 | } 44 | return nil 45 | } 46 | 47 | type BuildOptions struct { 48 | Config Config 49 | Tag string 50 | Force bool 51 | All_branches bool 52 | Long_sha bool 53 | Branch_tags bool 54 | Commit_tags bool 55 | } 56 | 57 | // Build function compiles the Containers of the project 58 | func Build(opts BuildOptions) { 59 | config := opts.Config 60 | 61 | var rev = getRevision(opts.Long_sha) 62 | 63 | // For each App 64 | for _, app := range config.GetApps() { 65 | // If no Git repo exist 66 | if !isGit() { 67 | // Perfoming [build latest] 68 | debug("No local git repository found, just building latest") 69 | 70 | // Execute Pre commands 71 | if res := Pre(app); res != nil { 72 | err("Pre execution returned non-zero status") 73 | return 74 | } 75 | 76 | // Build latest image 77 | res := buildImage(app, "latest", opts.Force) 78 | if res != nil { 79 | os.Exit(BuildFailed) 80 | } 81 | 82 | // Add additional user-defined Tag 83 | if opts.Tag != "" { 84 | tagImage(app, "latest", opts.Tag) 85 | } 86 | } else { 87 | // Skip build if there are no local changes and the commit is already built 88 | if !isDirty() && imageExist(app, rev) && !opts.Force { 89 | // Performing [skip rev|tag rev@latest|tag rev@branch] 90 | info("Skipping build of %s:%s - image is already built", app.Image, rev) 91 | 92 | // Tag commit image 93 | tagImage(app, rev, "latest") 94 | 95 | // Tag branch image 96 | for _, branch := range getBranches(opts.All_branches) { 97 | res := tagImage(app, rev, branch) 98 | if res != nil { 99 | os.Exit(TagFailed) 100 | } 101 | } 102 | 103 | // Add additional user-defined Tag 104 | if opts.Tag != "" { 105 | tagImage(app, rev, opts.Tag) 106 | } 107 | } else { 108 | // Performing [build latest|tag latest@rev|tag latest@branch] 109 | 110 | // Execute Pre commands 111 | if res := Pre(app); res != nil { 112 | err("Pre execution returned non-zero status") 113 | } 114 | 115 | // Build latest image 116 | res := buildImage(app, "latest", opts.Force) 117 | if res != nil { 118 | os.Exit(BuildFailed) 119 | } 120 | if isDirty() { 121 | debug("Skipping tag of %s:%s - local changes exist", app.Image, rev) 122 | } else { 123 | // Tag commit image 124 | tagImage(app, "latest", rev) 125 | 126 | // Tag branch image 127 | for _, branch := range getBranches(opts.All_branches) { 128 | res := tagImage(app, "latest", branch) 129 | if res != nil { 130 | os.Exit(TagFailed) 131 | } 132 | } 133 | 134 | // Add additional user-defined Tag 135 | if opts.Tag != "" { 136 | tagImage(app, rev, opts.Tag) 137 | } 138 | } 139 | } 140 | } 141 | 142 | // Execute Post commands 143 | if res := Post(app); res != nil { 144 | err("Post execution returned non-zero status") 145 | } 146 | } 147 | } 148 | 149 | // Test function executes the tests of the project 150 | func Test(opts BuildOptions) { 151 | config := opts.Config 152 | 153 | for _, app := range config.GetApps() { 154 | for _, value := range app.Test { 155 | info("Running test command: %s", value) 156 | res := execute("bash", "-c", value) 157 | if res != nil { 158 | err("Test execution returned non-zero status") 159 | os.Exit(ExecuteFailed) 160 | } 161 | } 162 | } 163 | } 164 | 165 | // Push function pushes the containers to the remote registry 166 | func Push(opts BuildOptions) { 167 | config := opts.Config 168 | 169 | // If no Git repo exist 170 | if !isGit() { 171 | err("No local git repository found, cannot push") 172 | os.Exit(NoGit) 173 | } 174 | 175 | if isDirty() { 176 | err("Git repository has local changes, cannot push") 177 | os.Exit(GitDirty) 178 | } 179 | 180 | for _, app := range config.GetApps() { 181 | for _, branch := range getBranches(opts.All_branches) { 182 | info("Pushing image %s:%s", app.Image, "latest") 183 | if res := pushImage(app.Image, "latest"); res != nil { 184 | err("Push returned non-zero status") 185 | os.Exit(ExecuteFailed) 186 | } 187 | if opts.Branch_tags { 188 | info("Pushing image %s:%s", app.Image, branch) 189 | if res := pushImage(app.Image, branch); res != nil { 190 | err("Push returned non-zero status") 191 | os.Exit(ExecuteFailed) 192 | } 193 | } 194 | if opts.Commit_tags { 195 | rev := getRevision(opts.Long_sha) 196 | info("Pushing image %s:%s", app.Image, rev) 197 | if res := pushImage(app.Image, rev); res != nil { 198 | err("Push returned non-zero status") 199 | os.Exit(ExecuteFailed) 200 | } 201 | } 202 | 203 | // Add additional user-defined Tag 204 | if opts.Tag != "" { 205 | info("Pushing image %s:%s", app.Image, opts.Tag) 206 | if res := pushImage(app.Image, opts.Tag); res != nil { 207 | err("Push returned non-zero status") 208 | os.Exit(ExecuteFailed) 209 | } 210 | } 211 | } 212 | } 213 | } 214 | 215 | // Pull function pulls the containers from the remote registry 216 | func Pull(opts BuildOptions) { 217 | config := opts.Config 218 | 219 | for _, app := range config.GetApps() { 220 | for _, branch := range getBranches(opts.All_branches) { 221 | info("Pulling image %s:%s", app.Image, "latest") 222 | if res := pullImage(app.Image, "latest"); res != nil { 223 | err("Pull returned non-zero status") 224 | os.Exit(ExecuteFailed) 225 | } 226 | if opts.Branch_tags { 227 | info("Pulling image %s:%s", app.Image, branch) 228 | if res := pullImage(app.Image, branch); res != nil { 229 | err("Pull returned non-zero status") 230 | os.Exit(ExecuteFailed) 231 | } 232 | } 233 | if opts.Commit_tags { 234 | rev := getRevision(opts.Long_sha) 235 | info("Pulling image %s:%s", app.Image, rev) 236 | if res := pullImage(app.Image, rev); res != nil { 237 | err("Pull returned non-zero status") 238 | os.Exit(ExecuteFailed) 239 | } 240 | } 241 | 242 | // Add additional user-defined Tag 243 | if opts.Tag != "" { 244 | info("Pulling image %s:%s", app.Image, opts.Tag) 245 | if res := pullImage(app.Image, opts.Tag); res != nil { 246 | err("Pull returned non-zero status") 247 | os.Exit(ExecuteFailed) 248 | } 249 | } 250 | } 251 | } 252 | } 253 | 254 | // Purge function purges the stale images 255 | func Purge(opts BuildOptions) { 256 | config := opts.Config 257 | 258 | // For each App 259 | for _, app := range config.GetApps() { 260 | var tags = []string{} 261 | 262 | // Retrieve the list of the existing Image tags 263 | for _, img := range getImages(app) { 264 | tags = append(tags, img.RepoTags...) 265 | } 266 | 267 | // Remove from the list: The latest image 268 | for key, tag := range tags { 269 | if tag == app.Image+":latest" { 270 | tags = append(tags[:key], tags[key+1:]...) 271 | } 272 | } 273 | 274 | // Remove from the list: The current commit-id 275 | for key, tag := range tags { 276 | if tag == app.Image+":"+getRevision(opts.Long_sha) { 277 | tags = append(tags[:key], tags[key+1:]...) 278 | } 279 | } 280 | 281 | // Remove from the list: The working-dir git branches 282 | for _, branch := range getBranches(opts.All_branches) { 283 | for key, tag := range tags { 284 | if tag == app.Image+":"+branch { 285 | tags = append(tags[:key], tags[key+1:]...) 286 | } 287 | } 288 | } 289 | 290 | // Proceed with deletion of Images 291 | for _, tag := range tags { 292 | info("Deleting image %s", tag) 293 | res := removeImage(tag) 294 | if res != nil { 295 | err("Deleting image failed: %s", res) 296 | os.Exit(DeleteImageFailed) 297 | } 298 | } 299 | } 300 | } 301 | 302 | func SelfUpdate() { 303 | captainDir := filepath.FromSlash(os.Getenv("HOME") + "/.captain") 304 | binariesDir := filepath.FromSlash(captainDir + "/binaries") 305 | binDir := filepath.FromSlash(captainDir + "/bin") 306 | 307 | kernel := runtime.GOOS 308 | arch := runtime.GOARCH 309 | captainSymlinkPath := filepath.FromSlash(binDir + "/captain") 310 | currentVersionPath, _ := os.Readlink(captainSymlinkPath) 311 | 312 | info("Checking the last version of Captain...") 313 | version := findLastVersion() 314 | downloadUrl := fmt.Sprintf("https://github.com/harbur/captain/releases/download/%s/captain_%s_%s", version, kernel, arch) 315 | downloadedVersionPath := filepath.FromSlash(binariesDir + "/captain-" + version) 316 | 317 | if currentVersionPath == downloadedVersionPath { 318 | info("You have installed the last version of captain (%s)", version) 319 | return 320 | } 321 | 322 | info("New version available, start downloading %s", version) 323 | 324 | // create binaries dir 325 | if err := os.MkdirAll(binariesDir, 0755); err != nil { 326 | fmt.Println(err) 327 | } 328 | 329 | // download new version 330 | if err := downloadFile(downloadedVersionPath, downloadUrl); err != nil { 331 | fmt.Println(err) 332 | } 333 | 334 | // grant excution to download version 335 | if err := os.Chmod(downloadedVersionPath, 0755); err != nil { 336 | fmt.Println(err) 337 | } 338 | 339 | info("Downloaded captain %s", version) 340 | 341 | if err := os.MkdirAll(binDir, 0755); err != nil { 342 | fmt.Println(err) 343 | } 344 | 345 | if _, err := os.Stat(captainSymlinkPath); err == nil { 346 | os.Remove(captainSymlinkPath) 347 | } 348 | 349 | if err := os.Symlink(downloadedVersionPath, captainSymlinkPath); err != nil { 350 | fmt.Println(err) 351 | } 352 | 353 | info("Installed captain %s", version) 354 | } 355 | 356 | func findLastVersion() string { 357 | url := "https://raw.githubusercontent.com/harbur/captain/master/VERSION" 358 | 359 | res, err := http.Get(url) 360 | 361 | if err != nil { 362 | panic(err) 363 | } 364 | defer res.Body.Close() 365 | 366 | content, err := ioutil.ReadAll(res.Body) 367 | if err != nil { 368 | panic(err) 369 | } 370 | 371 | return string(content) 372 | } 373 | 374 | func downloadFile(filepath string, url string) error { 375 | 376 | // Create the file 377 | out, err := os.Create(filepath) 378 | if err != nil { 379 | return err 380 | } 381 | defer out.Close() 382 | 383 | // Get the data 384 | resp, err := http.Get(url) 385 | if err != nil { 386 | return err 387 | } 388 | defer resp.Body.Close() 389 | 390 | // create and start bar 391 | bar := pb.New64(resp.ContentLength).SetUnits(pb.U_BYTES).Start() 392 | defer bar.Finish() 393 | 394 | // create proxy reader 395 | reader := bar.NewProxyReader(resp.Body) 396 | 397 | // and copy from pb reader 398 | _, err = io.Copy(out, reader) 399 | if err != nil { 400 | return err 401 | } 402 | 403 | return nil 404 | } 405 | -------------------------------------------------------------------------------- /captain.yml: -------------------------------------------------------------------------------- 1 | captain: 2 | build: Dockerfile 3 | image: harbur/captain 4 | test: 5 | - docker run -v /var/run/docker.sock:/var/run/docker.sock harbur/captain go test -v github.com/harbur/captain 6 | 7 | -------------------------------------------------------------------------------- /captain_test.go: -------------------------------------------------------------------------------- 1 | package captain // import "github.com/harbur/captain" 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | // Test Fixtures 9 | var validApp = App{ 10 | Image: "", 11 | Pre: []string{"echo running pre"}, 12 | Post: []string{"echo running post"}, 13 | } 14 | 15 | var invalidApp = App{ 16 | Pre: []string{"nonexistingPreCommand"}, 17 | Post: []string{"nonexistingPostCommand"}, 18 | } 19 | 20 | // Pre Command 21 | func TestPre(t *testing.T) { 22 | res := Pre(validApp) 23 | assert.Nil(t, res, "No error returned") 24 | } 25 | 26 | func TestPreFail(t *testing.T) { 27 | res := Pre(invalidApp) 28 | assert.NotNil(t, res, "Error returned") 29 | } 30 | 31 | // Post Command 32 | func TestPost(t *testing.T) { 33 | res := Post(validApp) 34 | assert.Nil(t, res, "No error returned") 35 | } 36 | 37 | func TestPostFail(t *testing.T) { 38 | res := Post(invalidApp) 39 | assert.NotNil(t, res, "Error returned") 40 | } 41 | 42 | // Build Command 43 | func TestBuild(t *testing.T) { 44 | var testConfig = readConfig(configFile(basedir + "/test/Simple/captain.yml")) 45 | 46 | var buildOpts = BuildOptions{ 47 | Config: testConfig, 48 | } 49 | 50 | Build(buildOpts) 51 | } 52 | 53 | // Test Command 54 | func TestTest(t *testing.T) { 55 | var testConfig = readConfig(configFile(basedir + "/test/Simple/captain.yml")) 56 | 57 | var buildOpts = BuildOptions{ 58 | Config: testConfig, 59 | } 60 | 61 | Test(buildOpts) 62 | } 63 | 64 | // Pull Command 65 | func TestPullNoBranchTags(t *testing.T) { 66 | var testConfig = readConfig(configFile(basedir + "/test/alpine/captain.yml")) 67 | 68 | var buildOpts = BuildOptions{ 69 | Config: testConfig, 70 | Branch_tags: false, 71 | } 72 | Pull(buildOpts) 73 | } 74 | 75 | // Purge Command 76 | func TestPurge(t *testing.T) { 77 | var testConfig = readConfig(configFile(basedir + "/test/alpine/captain.yml")) 78 | 79 | var buildOpts = BuildOptions{ 80 | Config: testConfig, 81 | } 82 | Purge(buildOpts) 83 | } 84 | 85 | // SelfUpdate Command 86 | func TestSelfUpdate(t *testing.T) { 87 | // First Time Self update 88 | SelfUpdate() 89 | // Already Installed last version 90 | SelfUpdate() 91 | } 92 | 93 | func TestDownloadFile(t *testing.T) { 94 | res := downloadFile("/tmp/captain.html", "https://github.com/harbur/captain") 95 | assert.Nil(t, res, "captain") 96 | } 97 | 98 | func TestFindLastVersion(t *testing.T) { 99 | res := findLastVersion() 100 | assert.NotNil(t, res, "Last version exists") 101 | } 102 | -------------------------------------------------------------------------------- /cmd/captain/cmd.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/fatih/color" 8 | "github.com/harbur/captain" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | // Options that are passed by CLI are mapped here for consumption 13 | type Options struct { 14 | debug bool 15 | force bool 16 | long_sha bool 17 | namespace string 18 | config string 19 | images []string 20 | tag string 21 | 22 | // Options to define the docker tags context 23 | all_branches bool 24 | branch_tags bool 25 | commit_tags bool 26 | } 27 | 28 | var options Options 29 | 30 | func handleCmd() { 31 | 32 | var cmdBuild = &cobra.Command{ 33 | Use: "build [image]", 34 | Short: "Builds the docker image(s) of your repository", 35 | Long: `It will build the docker image(s) described on captain.yml in order they appear on file.`, 36 | Run: func(cmd *cobra.Command, args []string) { 37 | config := captain.NewConfig(options.namespace, options.config, true) 38 | 39 | if len(args) == 1 { 40 | config.FilterConfig(args[0]) 41 | } 42 | 43 | buildOpts := captain.BuildOptions{ 44 | Config: config, 45 | Tag: options.tag, 46 | Force: options.force, 47 | All_branches: options.all_branches, 48 | Long_sha: options.long_sha, 49 | Branch_tags: options.branch_tags, 50 | Commit_tags: options.commit_tags, 51 | } 52 | 53 | captain.Build(buildOpts) 54 | }, 55 | } 56 | 57 | var cmdTest = &cobra.Command{ 58 | Use: "test", 59 | Short: "Runs the tests", 60 | Long: `It will execute the commands described on test section in order they appear on file.`, 61 | Run: func(cmd *cobra.Command, args []string) { 62 | config := captain.NewConfig(options.namespace, options.config, true) 63 | 64 | if len(args) == 1 { 65 | config.FilterConfig(args[0]) 66 | } 67 | 68 | buildOpts := captain.BuildOptions{ 69 | Config: config, 70 | Tag: options.tag, 71 | Force: options.force, 72 | All_branches: options.all_branches, 73 | Long_sha: options.long_sha, 74 | Branch_tags: options.branch_tags, 75 | Commit_tags: options.commit_tags, 76 | } 77 | 78 | // Build everything before testing 79 | captain.Build(buildOpts) 80 | captain.Test(buildOpts) 81 | }, 82 | } 83 | 84 | var cmdPush = &cobra.Command{ 85 | Use: "push", 86 | Short: "Pushes the images to remote registry", 87 | Long: `It will push the generated images to the remote registry.`, 88 | Run: func(cmd *cobra.Command, args []string) { 89 | config := captain.NewConfig(options.namespace, options.config, true) 90 | 91 | if len(args) == 1 { 92 | config.FilterConfig(args[0]) 93 | } 94 | 95 | buildOpts := captain.BuildOptions{ 96 | Config: config, 97 | Tag: options.tag, 98 | Force: options.force, 99 | All_branches: options.all_branches, 100 | Long_sha: options.long_sha, 101 | Branch_tags: options.branch_tags, 102 | Commit_tags: options.commit_tags, 103 | } 104 | 105 | // Build everything before pushing 106 | captain.Build(buildOpts) 107 | captain.Push(buildOpts) 108 | }, 109 | } 110 | 111 | var cmdPull = &cobra.Command{ 112 | Use: "pull", 113 | Short: "Pulls the images from remote registry", 114 | Long: `It will pull the images from the remote registry.`, 115 | Run: func(cmd *cobra.Command, args []string) { 116 | config := captain.NewConfig(options.namespace, options.config, true) 117 | 118 | if len(args) == 1 { 119 | config.FilterConfig(args[0]) 120 | } 121 | 122 | buildOpts := captain.BuildOptions{ 123 | Config: config, 124 | Tag: options.tag, 125 | Force: options.force, 126 | All_branches: options.all_branches, 127 | Long_sha: options.long_sha, 128 | Branch_tags: options.branch_tags, 129 | Commit_tags: options.commit_tags, 130 | } 131 | 132 | captain.Pull(buildOpts) 133 | }, 134 | } 135 | 136 | var cmdPurge = &cobra.Command{ 137 | Use: "purge", 138 | Short: "Purges the stale images", 139 | Long: `It will purge the stale images. Stale image is an image that is not the latest of at least one branch.`, 140 | Run: func(cmd *cobra.Command, args []string) { 141 | config := captain.NewConfig(options.namespace, options.config, true) 142 | 143 | if len(args) == 1 { 144 | config.FilterConfig(args[0]) 145 | } 146 | 147 | buildOpts := captain.BuildOptions{ 148 | Config: config, 149 | Force: options.force, 150 | All_branches: options.all_branches, 151 | Long_sha: options.long_sha, 152 | } 153 | 154 | captain.Purge(buildOpts) 155 | }, 156 | } 157 | 158 | var cmdSelfUpdate = &cobra.Command{ 159 | Use: "self-update", 160 | Short: "Updates Captain to the last version", 161 | Long: `Updates Captain to the last available version.`, 162 | Run: func(cmd *cobra.Command, args []string) { 163 | captain.SelfUpdate() 164 | }, 165 | } 166 | 167 | var cmdVersion = &cobra.Command{ 168 | Use: "version", 169 | Short: "Display version", 170 | Long: `Displays the version of Captain.`, 171 | Run: func(cmd *cobra.Command, args []string) { 172 | fmt.Println("v1.1.3") 173 | }, 174 | } 175 | 176 | var captainCmd = &cobra.Command{ 177 | Use: "captain", 178 | Short: "captain - build tool for Docker focused on CI/CD", 179 | Long: ` 180 | Captain, the CLI build tool for Docker made for Continuous Integration / Continuous Delivery. 181 | 182 | It works by reading captain.yaml file which describes how to build, test, push and release the docker image(s) of your repository.`, 183 | } 184 | 185 | captainCmd.PersistentFlags().BoolVarP(&captain.Debug, "debug", "D", false, "Enable debug mode") 186 | captainCmd.PersistentFlags().StringVarP(&options.namespace, "namespace", "N", getNamespace(), "Set default image namespace") 187 | captainCmd.PersistentFlags().BoolVarP(&color.NoColor, "no-color", "n", false, "Disable color output") 188 | captainCmd.PersistentFlags().BoolVarP(&options.long_sha, "long-sha", "l", false, "Use the long git commit SHA when referencing revisions") 189 | 190 | cmdBuild.Flags().BoolVarP(&options.force, "force", "f", false, "Force build even if image is already built") 191 | cmdBuild.Flags().BoolVarP(&options.all_branches, "all-branches", "B", false, "Build all branches on specific commit instead of just working branch") 192 | cmdBuild.Flags().StringVarP(&options.tag, "tag", "t", "", "Tag version") 193 | 194 | cmdPull.Flags().BoolVarP(&options.all_branches, "all-branches", "B", false, "Pull all branches on specific commit instead of just working branch") 195 | cmdPull.Flags().BoolVarP(&options.branch_tags, "branch-tags", "b", true, "Pull the 'branch' docker tags") 196 | cmdPull.Flags().BoolVarP(&options.commit_tags, "commit-tags", "c", false, "Pull the 'commit' docker tags") 197 | cmdPull.Flags().StringVarP(&options.tag, "tag", "t", "", "Tag version") 198 | 199 | cmdPush.Flags().BoolVarP(&options.all_branches, "all-branches", "B", false, "Push all branches on specific commit instead of just working branch") 200 | cmdPush.Flags().BoolVarP(&options.branch_tags, "branch-tags", "b", true, "Push the 'branch' docker tags") 201 | cmdPush.Flags().BoolVarP(&options.commit_tags, "commit-tags", "c", false, "Push the 'commit' docker tags") 202 | cmdPush.Flags().StringVarP(&options.tag, "tag", "t", "", "Tag version") 203 | 204 | cmdPurge.Flags().BoolVarP(&options.force, "dangling", "d", false, "Remove dangling images") 205 | 206 | captainCmd.AddCommand(cmdBuild, cmdTest, cmdPush, cmdPull, cmdVersion, cmdPurge, cmdSelfUpdate) 207 | captainCmd.Execute() 208 | } 209 | 210 | func getNamespace() string { 211 | return os.Getenv("USER") 212 | } 213 | -------------------------------------------------------------------------------- /cmd/captain/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func main() { 4 | handleCmd() 5 | } 6 | -------------------------------------------------------------------------------- /colors.go: -------------------------------------------------------------------------------- 1 | package captain // import "github.com/harbur/captain" 2 | 3 | import ( 4 | "github.com/fatih/color" 5 | ) 6 | 7 | var colorPrefix = color.New(color.FgWhite, color.Bold).SprintFunc() 8 | var colorDebug = color.New(color.FgBlue).SprintFunc() 9 | var colorInfo = color.New(color.FgGreen).SprintFunc() 10 | var colorWarn = color.New(color.FgYellow).SprintFunc() 11 | var colorErr = color.New(color.FgRed).SprintFunc() 12 | -------------------------------------------------------------------------------- /colors_test.go: -------------------------------------------------------------------------------- 1 | package captain // import "github.com/harbur/captain" 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestColorCodes(t *testing.T) { 10 | assert.Equal(t, "\x1b[32mhello\x1b[0m", colorInfo("hello"), "they should be equal") 11 | assert.Equal(t, "\x1b[33mhello\x1b[0m", colorWarn("hello"), "they should be equal") 12 | assert.Equal(t, "\x1b[31mhello\x1b[0m", colorErr("hello"), "they should be equal") 13 | } 14 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package captain // import "github.com/harbur/captain" 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | 12 | "gopkg.in/yaml.v2" 13 | ) 14 | 15 | // Config represents the information stored at captain.yml. It keeps information about images and unit tests. 16 | type Config interface { 17 | FilterConfig(filter string) bool 18 | GetApp(app string) App 19 | GetApps() []App 20 | } 21 | 22 | type configV1 struct { 23 | Build build 24 | Test map[string][]string 25 | Images []string 26 | Root []string 27 | } 28 | 29 | type build struct { 30 | Images map[string]string 31 | } 32 | 33 | type config map[string]App 34 | 35 | var configOrder *yaml.MapSlice 36 | 37 | // App struct 38 | type App struct { 39 | Build string 40 | Image string 41 | Pre []string 42 | Post []string 43 | Test []string 44 | Build_arg map[string]string 45 | } 46 | 47 | // configFile returns the file to read the config from. 48 | // If the --config option was given, 49 | // it will only use the given file. 50 | func configFile(path string) string { 51 | if len(path) > 0 { 52 | return path 53 | } 54 | return "captain.yml" 55 | } 56 | 57 | // readConfig will read the config file 58 | // and return the created config. 59 | func readConfig(filename string) *config { 60 | data, err := ioutil.ReadFile(filename) 61 | os.Chdir(filepath.Dir(filename)) 62 | if err != nil { 63 | panic(StatusError{err, 74}) 64 | } 65 | return unmarshal(data) 66 | } 67 | 68 | // displaySyntaxError will display more information 69 | // such as line and error type given an error and 70 | // the data that was unmarshalled. 71 | // Thanks to https://github.com/markpeek/packer/commit/5bf33a0e91b2318a40c42e9bf855dcc8dd4cdec5 72 | func displaySyntaxError(data []byte, syntaxError error) (err error) { 73 | syntax, ok := syntaxError.(*json.SyntaxError) 74 | if !ok { 75 | err = syntaxError 76 | return 77 | } 78 | newline := []byte{'\x0a'} 79 | space := []byte{' '} 80 | 81 | start, end := bytes.LastIndex(data[:syntax.Offset], newline)+1, len(data) 82 | if idx := bytes.Index(data[start:], newline); idx >= 0 { 83 | end = start + idx 84 | } 85 | 86 | line, pos := bytes.Count(data[:start], newline)+1, int(syntax.Offset)-start-1 87 | 88 | err = fmt.Errorf("\nError in line %d: %s \n%s\n%s^", line, syntaxError, data[start:end], bytes.Repeat(space, pos)) 89 | return 90 | } 91 | 92 | // unmarshal converts either JSON 93 | // or YAML into a config object. 94 | func unmarshal(data []byte) *config { 95 | var configV1 *configV1 96 | res := yaml.Unmarshal(data, &configV1) 97 | if len(configV1.Build.Images) > 0 { 98 | err("Old %s format detected! Please check the https://github.com/harbur/captain how to upgrade", "captain.yml") 99 | os.Exit(-1) 100 | } 101 | 102 | var config *config 103 | res = yaml.Unmarshal(data, &config) 104 | 105 | if res != nil { 106 | res = displaySyntaxError(data, res) 107 | err("%s", res) 108 | os.Exit(InvalidCaptainYML) 109 | } 110 | 111 | // We re-import it as MapSlice to keep order of apps 112 | res = yaml.Unmarshal(data, &configOrder) 113 | 114 | if res != nil { 115 | res = displaySyntaxError(data, res) 116 | err("%s", res) 117 | os.Exit(InvalidCaptainYML) 118 | } 119 | 120 | return config 121 | } 122 | 123 | // NewConfig returns a new Config instance based on the reading the captain.yml 124 | // file at path. 125 | // Containers will be ordered so that they can be 126 | // brought up and down with Docker. 127 | func NewConfig(namespace, path string, forceOrder bool) Config { 128 | var conf *config 129 | f := configFile(path) 130 | if _, err := os.Stat(f); err == nil { 131 | conf = readConfig(f) 132 | } 133 | 134 | if conf == nil { 135 | info("No configuration found %v - inferring values", configFile(path)) 136 | autoconf := make(config) 137 | conf = &autoconf 138 | dockerfiles := getDockerfiles(namespace) 139 | for build, image := range dockerfiles { 140 | autoconf[image] = App{Build: build, Image: image} 141 | } 142 | } 143 | 144 | var err error 145 | if err != nil { 146 | panic(StatusError{err, 78}) 147 | } 148 | return conf 149 | } 150 | 151 | // GetApps returns a list of Apps 152 | func (c *config) GetApps() []App { 153 | var cc = *c 154 | var apps []App 155 | if configOrder != nil { 156 | for _, v := range *configOrder { 157 | if val, ok := cc[v.Key.(string)]; ok { 158 | apps = append(apps, val) 159 | } 160 | } 161 | } else { 162 | for _, v := range *c { 163 | apps = append(apps, v) 164 | } 165 | } 166 | 167 | return apps 168 | } 169 | 170 | func (c *config) FilterConfig(filter string) bool { 171 | if filter != "" { 172 | res := false 173 | for key := range *c { 174 | if key == filter { 175 | res = true 176 | } else { 177 | delete(*c, key) 178 | } 179 | } 180 | return res 181 | } 182 | return true 183 | } 184 | 185 | // GetApp returns App configuration 186 | func (c *config) GetApp(app string) App { 187 | for key, k := range *c { 188 | if key == app { 189 | return k 190 | } 191 | } 192 | return App{} 193 | } 194 | 195 | // Global list, how can I pass it to the visitor pattern? 196 | var imagesMap = make(map[string]string) 197 | 198 | func getDockerfiles(namespace string) map[string]string { 199 | filepath.Walk(".", visit(namespace)) 200 | return imagesMap 201 | } 202 | 203 | func visit(namespace string) filepath.WalkFunc { 204 | return func(path string, f os.FileInfo, err error) error { 205 | // Filename is "Dockerfile" or has "Dockerfile." prefix and is not a directory 206 | if (f.Name() == "Dockerfile" || strings.HasPrefix(f.Name(), "Dockerfile.")) && !f.IsDir() { 207 | // Get Parent Dirname 208 | absolutePath, _ := filepath.Abs(path) 209 | var image = strings.ToLower(filepath.Base(filepath.Dir(absolutePath))) 210 | imagesMap[path] = namespace + "/" + image + strings.ToLower(filepath.Ext(path)) 211 | debug("Located %s will be used to create %s", path, imagesMap[path]) 212 | } 213 | return nil 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package captain // import "github.com/harbur/captain" 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | var basedir, _ = os.Getwd() 10 | 11 | func TestConfigFiles(t *testing.T) { 12 | c := configFile("captain.yml") 13 | sl := "captain.yml" 14 | assert.Equal(t, sl, c, "Should return possible config files") 15 | } 16 | 17 | func TestReadConfig(t *testing.T) { 18 | c := readConfig(configFile(basedir + "/test/Simple/captain.yml")) 19 | assert.NotNil(t, c, "Should return configuration") 20 | } 21 | 22 | func TestNewConfig(t *testing.T) { 23 | info("cwd %s", basedir) 24 | c := NewConfig("", basedir+"/test/Simple/captain.yml", false) 25 | assert.NotNil(t, c, "Should return captain.yml configuration") 26 | } 27 | 28 | func TestNewConfigInferringValues(t *testing.T) { 29 | c := NewConfig("", basedir+"/test/noCaptainYML/captain.yml", false) 30 | assert.NotNil(t, c, "Should return infered configuration") 31 | } 32 | 33 | func TestFilterConfigEmpty(t *testing.T) { 34 | c := NewConfig("", basedir+"/test/Simple/captain.yml", false) 35 | assert.Equal(t, 2, len(c.GetApps()), "Should return 2 apps") 36 | 37 | res := c.FilterConfig("") 38 | assert.True(t, res, "Should return true") 39 | assert.Equal(t, 2, len(c.GetApps()), "Should return 2 apps") 40 | } 41 | 42 | func TestFilterConfigNonExistent(t *testing.T) { 43 | c := NewConfig("", basedir+"/test/Simple/captain.yml", false) 44 | assert.Equal(t, 2, len(c.GetApps()), "Should return 2 apps") 45 | 46 | res := c.FilterConfig("nonexistent") 47 | assert.False(t, res, "Should return false") 48 | assert.Equal(t, 0, len(c.GetApps()), "Should return 0 apps") 49 | } 50 | 51 | func TestFilterConfigWeb(t *testing.T) { 52 | c := NewConfig("", basedir+"/test/Simple/captain.yml", false) 53 | assert.Equal(t, 2, len(c.GetApps()), "Should return 2 apps") 54 | 55 | c.FilterConfig("web") 56 | assert.Equal(t, 1, len(c.GetApps()), "Should return 1 app") 57 | assert.Equal(t, "Dockerfile", c.GetApp("web").Build, "Should return web Build field") 58 | } 59 | 60 | func TestGetApp(t *testing.T) { 61 | c := NewConfig("", basedir+"/test/Simple/captain.yml", false) 62 | app := c.GetApp("web") 63 | assert.Equal(t, "harbur/test_web", app.Image, "Should return web image") 64 | } 65 | -------------------------------------------------------------------------------- /docker.go: -------------------------------------------------------------------------------- 1 | package captain // import "github.com/harbur/captain" 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/fsouza/go-dockerclient" 9 | ) 10 | 11 | const defaultEndpoint = "unix:///var/run/docker.sock" 12 | 13 | var client *docker.Client 14 | 15 | func init() { 16 | var err error 17 | client, err = docker.NewClientFromEnv() 18 | if err != nil { 19 | panic(err) 20 | } 21 | } 22 | 23 | type BuildArgSet struct { 24 | slice []docker.BuildArg 25 | } 26 | 27 | func buildImage(app App, tag string, force bool) error { 28 | info("Building image %s:%s", app.Image, tag) 29 | 30 | // Nasty issue with CircleCI https://github.com/docker/docker/issues/4897 31 | if os.Getenv("CIRCLECI") == "true" { 32 | info("Running at %s environment...", "CIRCLECI") 33 | execute("docker", "build", "-t", app.Image+":"+tag, filepath.Dir(app.Build)) 34 | return nil 35 | } 36 | 37 | // Create BuildArg set 38 | buildArgSet := BuildArgSet{(make([]docker.BuildArg, 0, 10))} 39 | if len(app.Build_arg) > 0 { 40 | for k, arg := range app.Build_arg { 41 | buildArgSet.slice = append(buildArgSet.slice, docker.BuildArg{Name: k, Value: arg}) 42 | } 43 | } 44 | opts := docker.BuildImageOptions{ 45 | Name: app.Image + ":" + tag, 46 | Dockerfile: filepath.Base(app.Build), 47 | NoCache: force, 48 | SuppressOutput: false, 49 | RmTmpContainer: true, 50 | ForceRmTmpContainer: true, 51 | OutputStream: os.Stdout, 52 | ContextDir: filepath.Dir(app.Build), 53 | BuildArgs: buildArgSet.slice, 54 | } 55 | 56 | // Use ~/.docker/ auth configuration if exists 57 | dockercfg, _ := docker.NewAuthConfigurationsFromDockerCfg() 58 | if dockercfg != nil { 59 | opts.AuthConfigs = *dockercfg 60 | } 61 | 62 | err := client.BuildImage(opts) 63 | if err != nil { 64 | fmt.Printf("%s", err) 65 | } 66 | return err 67 | } 68 | 69 | func pushImage(image string, version string) error { 70 | return execute("docker", "push", image+":"+version) 71 | } 72 | 73 | func pullImage(image string, version string) error { 74 | return execute("docker", "pull", image+":"+version) 75 | } 76 | 77 | func tagImage(app App, origin string, tag string) error { 78 | if tag != "" { 79 | info("Tagging image %s:%s as %s:%s", app.Image, origin, app.Image, tag) 80 | opts := docker.TagImageOptions{Repo: app.Image, Tag: tag, Force: true} 81 | err := client.TagImage(app.Image+":"+origin, opts) 82 | if err != nil { 83 | fmt.Printf("%s", err) 84 | } 85 | return err 86 | } 87 | 88 | debug("Skipping tag of %s - no git repository", app.Image) 89 | 90 | return nil 91 | } 92 | 93 | func removeImage(name string) error { 94 | return client.RemoveImage(name) 95 | } 96 | 97 | /** 98 | * Retrieves a list of existing Images for the specific App. 99 | */ 100 | func getImages(app App) []docker.APIImages { 101 | debug("Getting images %s", app.Image) 102 | imgs, _ := client.ListImages(docker.ListImagesOptions{All: false, Filter: app.Image}) 103 | return imgs 104 | } 105 | 106 | func imageExist(app App, tag string) bool { 107 | repo := app.Image + ":" + tag 108 | image, _ := client.InspectImage(repo) 109 | if image != nil { 110 | return true 111 | } 112 | return false 113 | } 114 | -------------------------------------------------------------------------------- /docker_test.go: -------------------------------------------------------------------------------- 1 | package captain // import "github.com/harbur/captain" 2 | 3 | import ( 4 | "testing" 5 | 6 | "os" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestBuildImage(t *testing.T) { 12 | app := App{Build: basedir + "/test/noCaptainYML/Dockerfile", Image: "captain_test"} 13 | res := buildImage(app, "latest", false) 14 | assert.Nil(t, res, "Docker build should not return any error") 15 | } 16 | 17 | func TestBuildImageError(t *testing.T) { 18 | app := App{Build: basedir + "/test/noCaptainYML/Dockerfile.error", Image: "captain_test"} 19 | res := buildImage(app, "latest", false) 20 | assert.NotNil(t, res, "Docker build should return an error") 21 | } 22 | 23 | func TestBuildImageCircleCI(t *testing.T) { 24 | os.Setenv("CIRCLECI", "true") 25 | app := App{Build: basedir + "/test/noCaptainYML/Dockerfile", Image: "captain_test"} 26 | res := buildImage(app, "latest", false) 27 | assert.Nil(t, res, "Docker build should not return any error") 28 | } 29 | 30 | func TestTagImage(t *testing.T) { 31 | app := App{Image: "golang"} 32 | res := tagImage(app, "1.4.2", "testing") 33 | assert.Nil(t, res, "Docker tag should not return any error") 34 | } 35 | 36 | func TestTagNonexistingImage(t *testing.T) { 37 | app := App{Image: "golang"} 38 | res := tagImage(app, "nonexist", "testing") 39 | assert.NotNil(t, res, "Docker tag should return an error") 40 | println() 41 | } 42 | 43 | func TestImageExist(t *testing.T) { 44 | app := App{Image: "golang"} 45 | exist := imageExist(app, "1.4.2") 46 | assert.Equal(t, true, exist, "Docker image golang:1.4.2 should exist") 47 | } 48 | 49 | func TestImageDoesNotExist(t *testing.T) { 50 | app := App{Image: "golang"} 51 | exist := imageExist(app, "nonexist") 52 | assert.Equal(t, false, exist, "Docker image golang:nonexist should not exist") 53 | } 54 | -------------------------------------------------------------------------------- /execute.go: -------------------------------------------------------------------------------- 1 | package captain // import "github.com/harbur/captain" 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "os/exec" 7 | "strings" 8 | ) 9 | 10 | func execute(name string, arg ...string) error { 11 | // Construct command for debug purposes 12 | var command = name 13 | for _, i := range arg { 14 | command += " " + i 15 | } 16 | 17 | debug("Executing %s", command) 18 | cmd := exec.Command(name, arg...) 19 | cmd.Stdout = os.Stdout 20 | cmd.Stderr = os.Stderr 21 | cmd.Stdin = os.Stdin 22 | return cmd.Run() 23 | } 24 | 25 | func oneliner(name string, arg ...string) (string, error) { 26 | var buff bytes.Buffer 27 | gitCmd := exec.Command(name, arg...) 28 | gitCmd.Stdout = &buff 29 | err := gitCmd.Run() 30 | return strings.TrimSpace(buff.String()), err 31 | } 32 | -------------------------------------------------------------------------------- /execute_test.go: -------------------------------------------------------------------------------- 1 | package captain // import "github.com/harbur/captain" 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestExecute(t *testing.T) { 10 | res := execute("echo", "testing") 11 | assert.Equal(t, nil, res, "it should execute without errors") 12 | } 13 | 14 | func TestOneliner(t *testing.T) { 15 | res, _ := oneliner("echo", "testing") 16 | assert.Equal(t, "testing", res, "it should return the trimmed result") 17 | } 18 | 19 | func TestOnelinerTrimmed(t *testing.T) { 20 | res, _ := oneliner("echo", "testing with spaces ") 21 | assert.Equal(t, "testing with spaces", res, "it should return the trimmed result") 22 | } 23 | -------------------------------------------------------------------------------- /exitstatus.go: -------------------------------------------------------------------------------- 1 | package captain // import "github.com/harbur/captain" 2 | 3 | const ( 4 | // BuildFailed represents a build failure 5 | BuildFailed = 1 6 | 7 | // TagFailed represents a failure to tag a docker image 8 | TagFailed = 2 9 | 10 | // NonExistImage represents the existance of a docker image tag 11 | NonExistImage = 3 12 | 13 | // TestFailed represents test failure 14 | TestFailed = 5 15 | 16 | // NoGit represents lack of a git repository 17 | NoGit = 6 18 | 19 | // GitDirty represents existence of local git changes 20 | GitDirty = 7 21 | 22 | // InvalidCaptainYML represents an invalid captain.yml format 23 | InvalidCaptainYML = 8 24 | 25 | // NoDockerfiles represents lack of Dockerfile(s) on current and subdirectories. 26 | NoDockerfiles = 9 27 | 28 | // OldFormat represents old format of captain.yml 29 | OldFormat = 10 30 | 31 | // DeleteImageFailed represents failure during image deletion 32 | DeleteImageFailed = 11 33 | 34 | // ExecuteFailed represents an execution failure 35 | ExecuteFailed = 12 36 | ) 37 | -------------------------------------------------------------------------------- /git.go: -------------------------------------------------------------------------------- 1 | package captain // import "github.com/harbur/captain" 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | func getRevision(long_sha bool) string { 9 | params := []string{"rev-parse"} 10 | if !long_sha { 11 | params = append(params, "--short") 12 | } 13 | 14 | params = append(params, "HEAD") 15 | res, _ := oneliner("git", params...) 16 | return res 17 | } 18 | 19 | func getBranches(all_branches bool) []string { 20 | // Labels (branches + tags) 21 | var labels = []string{} 22 | 23 | branches_str, _ := oneliner("git", "name-rev", "--name-only", "--exclude=tags/*", "HEAD") 24 | if all_branches { 25 | branches_str, _ = oneliner("git", "branch", "--no-column", "--contains", "HEAD") 26 | } 27 | 28 | var branches = make([]string, 5) 29 | if branches_str != "" { 30 | // Remove asterisk from branches list 31 | r := regexp.MustCompile("[\\* ] ") 32 | branches_str = r.ReplaceAllString(branches_str, "") 33 | branches = strings.Split(branches_str, "\n") 34 | 35 | // Branches list is separated by spaces. Let's put it in an array 36 | labels = append(labels, branches...) 37 | } 38 | 39 | tags_str, _ := oneliner("git", "tag", "--points-at", "HEAD") 40 | 41 | if tags_str != "" { 42 | tags := strings.Split(tags_str, "\n") 43 | debug("Active branches %s and tags %s", branches, tags) 44 | // Git tag list is separated by multi-lines. Let's put it in an array 45 | labels = append(labels, tags...) 46 | } 47 | 48 | for key := range labels { 49 | // Remove start of "heads/origin" if exist 50 | r := regexp.MustCompile("^heads\\/origin\\/") 51 | labels[key] = r.ReplaceAllString(labels[key], "") 52 | 53 | // Remove start of "remotes/origin" if exist 54 | r = regexp.MustCompile("^remotes\\/origin\\/") 55 | labels[key] = r.ReplaceAllString(labels[key], "") 56 | 57 | // Replace all "/" with "." 58 | labels[key] = strings.Replace(labels[key], "/", ".", -1) 59 | 60 | // Replace all "~" with "." 61 | labels[key] = strings.Replace(labels[key], "~", ".", -1) 62 | } 63 | 64 | return labels 65 | } 66 | 67 | func isDirty() bool { 68 | res, _ := oneliner("git", "status", "--porcelain") 69 | return len(res) > 0 70 | } 71 | 72 | func isGit() bool { 73 | res := getRevision(false) 74 | return len(res) > 0 75 | } 76 | -------------------------------------------------------------------------------- /git_test.go: -------------------------------------------------------------------------------- 1 | package captain // import "github.com/harbur/captain" 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestGitGetRevision(t *testing.T) { 10 | assert.Equal(t, 7, len(getRevision(false)), "Git revision should have length 7 chars") 11 | } 12 | 13 | func TestGitGetRevisionFullSha(t *testing.T) { 14 | assert.Equal(t, 40, len(getRevision(true)), "Git revision should have a length of 40 chars") 15 | } 16 | 17 | // TODO Fails because it assumes current branch is master 18 | func TestGitGetBranch(t *testing.T) { 19 | // assert.Equal(t, []string{"master"}, getBranches(false), "Git branch should be master") 20 | } 21 | 22 | // TODO Fails because it assumes current branch is master 23 | func TestGitGetBranchAllBranches(t *testing.T) { 24 | // assert.Equal(t, []string{"master"}, getBranches(true), "Git branch should be master") 25 | } 26 | 27 | // TODO Fails because vendors/ is not git-ignored. 28 | func TestGitIsDirty(t *testing.T) { 29 | // assert.Equal(t, false, isDirty(), "Git should not have local changes") 30 | } 31 | 32 | func TestGitIsGit(t *testing.T) { 33 | assert.Equal(t, true, isGit(), "There should be a git repository") 34 | } 35 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/harbur/captain 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect 7 | github.com/Microsoft/go-winio v0.4.5 // indirect 8 | github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect 9 | github.com/containerd/continuity v0.0.0-20171004134916-1bed1ecb1dc4 // indirect 10 | github.com/davecgh/go-spew v1.1.1 // indirect 11 | github.com/docker/docker v0.0.0-20171109040201-d4239a6e286f // indirect 12 | github.com/docker/go-connections v0.3.0 // indirect 13 | github.com/docker/go-units v0.3.2 // indirect 14 | github.com/fatih/color v0.0.0-20170926111411-5df930a27be2 15 | github.com/fsouza/go-dockerclient v0.0.0-20171104153632-ef22af91edfe 16 | github.com/gogo/protobuf v0.0.0-20171108112821-3813b83578b9 // indirect 17 | github.com/google/go-cmp v0.3.1 // indirect 18 | github.com/gotestyourself/gotestyourself v2.2.0+incompatible // indirect 19 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 20 | github.com/kr/pretty v0.1.0 // indirect 21 | github.com/mattn/go-colorable v0.0.0-20170210172801-5411d3eea597 // indirect 22 | github.com/mattn/go-isatty v0.0.0-20170307163044-57fdcb988a5c // indirect 23 | github.com/mattn/go-runewidth v0.0.0-20170510074858-97311d9f7767 // indirect 24 | github.com/onsi/ginkgo v1.10.2 // indirect 25 | github.com/onsi/gomega v1.7.0 // indirect 26 | github.com/opencontainers/go-digest v1.0.0-rc1 // indirect 27 | github.com/opencontainers/image-spec v0.0.0-20171103113604-89b51c794e91 // indirect 28 | github.com/opencontainers/runc v0.0.0-20171108154827-b2567b37d7b7 // indirect 29 | github.com/opencontainers/selinux v1.3.0 // indirect 30 | github.com/pkg/errors v0.0.0-20171018195549-f15c970de5b7 // indirect 31 | github.com/pmezard/go-difflib v1.0.0 // indirect 32 | github.com/sirupsen/logrus v0.0.0-20170822132746-89742aefa4b2 // indirect 33 | github.com/spf13/cobra v0.0.0-20150521034341-8f5946caaeef 34 | github.com/spf13/pflag v0.0.0-20171106142849-4c012f6dcd95 // indirect 35 | github.com/stretchr/testify v0.0.0-20171018052257-2aa2c176b9da 36 | golang.org/x/crypto v0.0.0-20171108091819-6a293f2d4b14 // indirect 37 | gopkg.in/airbrake/gobrake.v2 v2.0.9 // indirect 38 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 39 | gopkg.in/cheggaaa/pb.v1 v1.0.18 40 | gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 // indirect 41 | gopkg.in/yaml.v2 v2.2.1 42 | gotest.tools v2.2.0+incompatible // indirect 43 | ) 44 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= 2 | github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= 3 | github.com/Microsoft/go-winio v0.4.5 h1:U2XsGR5dBg1yzwSEJoP2dE2/aAXpmad+CNG2hE9Pd5k= 4 | github.com/Microsoft/go-winio v0.4.5/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= 5 | github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= 6 | github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= 7 | github.com/containerd/continuity v0.0.0-20171004134916-1bed1ecb1dc4 h1:OPNlxOq7hyfpwmo4OM8R0FCWIBvkeXn6j76ssVnlfDs= 8 | github.com/containerd/continuity v0.0.0-20171004134916-1bed1ecb1dc4/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/docker/docker v0.0.0-20171109040201-d4239a6e286f h1:OPD5tFOJmtHOnix/vnCunbzyZy3i+KUNU6fD7HL9+So= 12 | github.com/docker/docker v0.0.0-20171109040201-d4239a6e286f/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 13 | github.com/docker/go-connections v0.3.0 h1:3lOnM9cSzgGwx8VfK/NGOW5fLQ0GjIlCkaktF+n1M6o= 14 | github.com/docker/go-connections v0.3.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= 15 | github.com/docker/go-units v0.3.2 h1:Kjm80apys7gTtfVmCvVY8gwu10uofaFSrmAKOVrtueE= 16 | github.com/docker/go-units v0.3.2/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 17 | github.com/fatih/color v0.0.0-20170926111411-5df930a27be2 h1:40J76vs1Y7oiHFqTrQHQ6A5u8vbXJdLaMkC9iHU/uMw= 18 | github.com/fatih/color v0.0.0-20170926111411-5df930a27be2/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 19 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 20 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 21 | github.com/fsouza/go-dockerclient v0.0.0-20171104153632-ef22af91edfe h1:ZrUGkslX9sb9nu3RID+klbH1fst/uaZ4bEyuFaE+NdM= 22 | github.com/fsouza/go-dockerclient v0.0.0-20171104153632-ef22af91edfe/go.mod h1:KpcjM623fQYE9MZiTGzKhjfxXAV9wbyX2C1cyRHfhl0= 23 | github.com/gogo/protobuf v0.0.0-20171108112821-3813b83578b9 h1:l3Bt/o8ePrcrljRFye2X+ZGes/+pySkCRfgm/BEPC4I= 24 | github.com/gogo/protobuf v0.0.0-20171108112821-3813b83578b9/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 25 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 26 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 27 | github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= 28 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 29 | github.com/gotestyourself/gotestyourself v2.2.0+incompatible h1:AQwinXlbQR2HvPjQZOmDhRqsv5mZf+Jb1RnSLxcqZcI= 30 | github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY= 31 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 32 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 33 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 34 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 35 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 36 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 37 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 38 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 39 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 40 | github.com/mattn/go-colorable v0.0.0-20170210172801-5411d3eea597 h1:hGizH4aMDFFt1iOA4HNKC13lqIBoCyxIjWcAnWIy7aU= 41 | github.com/mattn/go-colorable v0.0.0-20170210172801-5411d3eea597/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 42 | github.com/mattn/go-isatty v0.0.0-20170307163044-57fdcb988a5c h1:AHfQR/s6GNi92TOh+kfGworqDvTxj2rMsS+Hca87nck= 43 | github.com/mattn/go-isatty v0.0.0-20170307163044-57fdcb988a5c/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 44 | github.com/mattn/go-runewidth v0.0.0-20170510074858-97311d9f7767 h1:Nk2R0tWpD2RdkQ+53zE6kWnSGuhQyDlnOs2MPiqVubE= 45 | github.com/mattn/go-runewidth v0.0.0-20170510074858-97311d9f7767/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 46 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 47 | github.com/onsi/ginkgo v1.10.2 h1:uqH7bpe+ERSiDa34FDOF7RikN6RzXgduUF8yarlZp94= 48 | github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 49 | github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= 50 | github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 51 | github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ= 52 | github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= 53 | github.com/opencontainers/image-spec v0.0.0-20171103113604-89b51c794e91 h1:WUilY9pRrZqn3ZV+NUB7tTiO1Yl/CjYgLHShs4+BbOM= 54 | github.com/opencontainers/image-spec v0.0.0-20171103113604-89b51c794e91/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= 55 | github.com/opencontainers/runc v0.0.0-20171108154827-b2567b37d7b7 h1:xhXFCGbdNF/tNT/sqOURMzRrAaSprZjR56sIqZ311SI= 56 | github.com/opencontainers/runc v0.0.0-20171108154827-b2567b37d7b7/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= 57 | github.com/opencontainers/selinux v1.3.0 h1:xsI95WzPZu5exzA6JzkLSfdr/DilzOhCJOqGe5TgR0g= 58 | github.com/opencontainers/selinux v1.3.0/go.mod h1:+BLncwf63G4dgOzykXAxcmnFlUaOlkDdmw/CqsW6pjs= 59 | github.com/pkg/errors v0.0.0-20171018195549-f15c970de5b7 h1:rRublLXoszYPRZV8Ikd3RTmqVCW289H3FsgqRcfDZhY= 60 | github.com/pkg/errors v0.0.0-20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 61 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 62 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 63 | github.com/sirupsen/logrus v0.0.0-20170822132746-89742aefa4b2 h1:+8J/sCAVv2Y9Ct1BKszDFJEVWv6Aynr2O4FYGUg6+Mc= 64 | github.com/sirupsen/logrus v0.0.0-20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= 65 | github.com/spf13/cobra v0.0.0-20150521034341-8f5946caaeef h1:wp/6bIkvqQ1h7cgs1b+U5x49tP+aSN8q4sC6es2FDcg= 66 | github.com/spf13/cobra v0.0.0-20150521034341-8f5946caaeef/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 67 | github.com/spf13/pflag v0.0.0-20171106142849-4c012f6dcd95 h1:fBkxrj/ArtKnC3J1DOZhn3SYiVkVRFZC574bq2Ifa/0= 68 | github.com/spf13/pflag v0.0.0-20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 69 | github.com/stretchr/testify v0.0.0-20171018052257-2aa2c176b9da h1:/GRWXYJLcWpnjmFCtD64tuNT1YteX36zELue/SXxl5Y= 70 | github.com/stretchr/testify v0.0.0-20171018052257-2aa2c176b9da/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 71 | golang.org/x/crypto v0.0.0-20171108091819-6a293f2d4b14 h1:zZpc/2lPMz1dJJQXHILHatH4bsh0vdDRwweWSHHbfuA= 72 | golang.org/x/crypto v0.0.0-20171108091819-6a293f2d4b14/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 73 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= 74 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 75 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= 76 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 77 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs= 78 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 79 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 80 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 81 | gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo= 82 | gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= 83 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 84 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 85 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 86 | gopkg.in/cheggaaa/pb.v1 v1.0.18 h1:h5Qflf8N54NDtm3lWfBuCD4rslDjkXDoGkEMZCH4R80= 87 | gopkg.in/cheggaaa/pb.v1 v1.0.18/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= 88 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 89 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 90 | gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 h1:OAj3g0cR6Dx/R07QgQe8wkA9RNjB2u4i700xBkIT4e0= 91 | gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= 92 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 93 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 94 | gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= 95 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 96 | gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= 97 | gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= 98 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | function getDistributionTag { 6 | local ext="" 7 | if [[ `uname -m` == "386" ]]; then 8 | machine="386" 9 | else 10 | machine="amd64" 11 | fi 12 | 13 | if [[ `uname -s` == "Darwin" ]]; then 14 | kernel="darwin" 15 | elif [[ `uname -s` =~ CYGWIN*|MINGW32*|MSYS* ]]; then 16 | kernel="windows" 17 | ext=".exe" 18 | else 19 | kernel="linux" 20 | fi 21 | 22 | echo "captain_${kernel}_${machine}${ext}" 23 | } 24 | 25 | CAPTAIN_DIR=$HOME/.captain 26 | CAPTAIN_BIN_DIR=$CAPTAIN_DIR/bin 27 | CAPTAIN_BINARIES_DIR=$CAPTAIN_DIR/binaries 28 | CAPTAIN_CURRENT_VERSION_URL=$(curl -sS https://raw.githubusercontent.com/harbur/captain/master/VERSION) 29 | CAPTAIN_CURRENT_VERSION_PATH="${CAPTAIN_BINARIES_DIR}/captain-${CAPTAIN_CURRENT_VERSION_URL}" 30 | CAPTAIN_DISTRIBUTION=$(getDistributionTag) 31 | 32 | 33 | echo "Creating folders in ${CAPTAIN_DIR}" 34 | mkdir -p $CAPTAIN_BIN_DIR $CAPTAIN_BINARIES_DIR 35 | 36 | echo "Start downloading Captain ${CAPTAIN_CURRENT_VERSION_URL}" 37 | curl -sSL https://github.com/harbur/captain/releases/download/${CAPTAIN_CURRENT_VERSION_URL}/${CAPTAIN_DISTRIBUTION} > ${CAPTAIN_CURRENT_VERSION_PATH} 38 | ln -snf ${CAPTAIN_CURRENT_VERSION_PATH} "${CAPTAIN_BIN_DIR}/captain" 39 | chmod +x "${CAPTAIN_BIN_DIR}/captain" 40 | 41 | echo "Captain ${CAPTAIN_CURRENT_VERSION_URL} installed" 42 | echo "" 43 | echo "IMPORTANT: Add ${CAPTAIN_BIN_DIR} in your path. E.g.:" 44 | echo "export PATH=${CAPTAIN_BIN_DIR}:\$PATH" 45 | 46 | -------------------------------------------------------------------------------- /print.go: -------------------------------------------------------------------------------- 1 | package captain // import "github.com/harbur/captain" 2 | 3 | import "fmt" 4 | 5 | func info(text string, arg ...interface{}) { 6 | text = colorInfo("[") + colorPrefix("CAPTAIN") + colorInfo("]") + " " + text + "\n" 7 | s := arg 8 | for i := range s { 9 | s[i] = colorInfo(s[i]) 10 | } 11 | fmt.Printf(text, arg...) 12 | } 13 | 14 | func err(text string, arg ...interface{}) { 15 | text = colorErr("[") + colorPrefix("CAPTAIN") + colorErr("]") + " " + text + "\n" 16 | s := arg 17 | for i := range s { 18 | s[i] = colorErr(s[i]) 19 | } 20 | fmt.Printf(text, s...) 21 | } 22 | 23 | func debug(text string, arg ...interface{}) { 24 | if Debug { 25 | text = colorDebug("[") + colorPrefix("CAPTAIN") + colorDebug("]") + " " + text + "\n" 26 | s := arg 27 | for i := range s { 28 | s[i] = colorDebug(s[i]) 29 | } 30 | fmt.Printf(text, s...) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /print_test.go: -------------------------------------------------------------------------------- 1 | package captain // import "github.com/harbur/captain" 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestPrintInfo(t *testing.T) { 8 | info("test info %s", "message") 9 | } 10 | 11 | func TestPrintErr(t *testing.T) { 12 | err("test err %s", "message") 13 | } 14 | 15 | func TestPrintDebug(t *testing.T) { 16 | Debug = true 17 | defer func() { Debug = false }() 18 | debug("test debug %s", "message") 19 | } 20 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | version=$1 6 | 7 | if [ -z "$version" ]; then 8 | echo "No version passed! Example usage: ./release.sh 1.0.0" 9 | exit 1 10 | fi 11 | 12 | echo "Running tests..." 13 | go test 14 | 15 | echo "Update version..." 16 | echo -n "v${version}" > VERSION 17 | sed -i '' 's/fmt\.Println("v[0-9]*\.[0-9]*\.[0-9]*")/fmt.Println("v'$version'")/' cmd/captain/cmd.go 18 | sed -i '' 's/v[0-9]*\.[0-9]*\.[0-9]*/v'$version'/' README.md 19 | 20 | echo "Build binaries..." 21 | make cross 22 | 23 | echo "Update repository..." 24 | git add cmd/captain/cmd.go README.md VERSION 25 | git commit -m "Preparing version ${version}" 26 | git tag --message="v$version" "v$version" 27 | 28 | echo "v$version tagged." 29 | echo "Now, run 'git push origin master && git push --tags' and publish the release on GitHub." 30 | -------------------------------------------------------------------------------- /test/OneImage/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.4.2 -------------------------------------------------------------------------------- /test/OneImage/captain.yml: -------------------------------------------------------------------------------- 1 | web: 2 | build: Dockerfile 3 | image: harbur/test_web 4 | test: 5 | - echo testing 1 web 6 | pre: 7 | - echo pre-action 1 web 8 | post: 9 | - echo post-action 1 web 10 | -------------------------------------------------------------------------------- /test/Simple/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.4.2 2 | -------------------------------------------------------------------------------- /test/Simple/Dockerfile.backend: -------------------------------------------------------------------------------- 1 | FROM golang:1.4.2 2 | -------------------------------------------------------------------------------- /test/Simple/captain.yml: -------------------------------------------------------------------------------- 1 | web: 2 | build: Dockerfile 3 | image: harbur/test_web 4 | test: 5 | - echo testing 1 web 6 | - echo testing 2 web 7 | pre: 8 | - echo pre-action 1 web 9 | - echo pre-action 2 web 10 | post: 11 | - echo post-action 1 web 12 | - echo post-action 2 web 13 | backend: 14 | build: Dockerfile.backend 15 | image: harbur/test_backend 16 | test: 17 | - echo testing 1 backend 18 | - echo testing 2 backend 19 | pre: 20 | - echo pre-action 1 backend 21 | - echo pre-action 2 backend 22 | post: 23 | - echo post-action 1 backend 24 | - echo post-action 2 backend -------------------------------------------------------------------------------- /test/alpine/captain.yml: -------------------------------------------------------------------------------- 1 | alpine: 2 | image: alpine 3 | -------------------------------------------------------------------------------- /test/buildArgs/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.3 2 | ARG NODE_ENV=production 3 | RUN echo $NODE_ENV > /etc/node_env 4 | CMD ["sh", "-c", "cat /etc/node_env"] 5 | -------------------------------------------------------------------------------- /test/buildArgs/captain.yml: -------------------------------------------------------------------------------- 1 | buildargs: 2 | build: Dockerfile 3 | image: harbur/buildargs 4 | build_arg: 5 | NODE_ENV: production 6 | buildargs-test: 7 | build: Dockerfile 8 | image: harbur/buildargs-test 9 | build_arg: 10 | NODE_ENV: test 11 | -------------------------------------------------------------------------------- /test/noCaptainYML/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.4.2 2 | -------------------------------------------------------------------------------- /test/noCaptainYML/Dockerfile.error: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harbur/captain/ee4f567b88d0db9fb653623a5418597ba021ffde/test/noCaptainYML/Dockerfile.error --------------------------------------------------------------------------------