├── fakes └── docker │ ├── fail_run.go │ ├── fail_pull.go │ ├── dockerconfig │ └── constants.go │ └── main.go ├── .gitmodules ├── go.mod ├── piper ├── fixtures │ ├── task.yml │ └── advanced_task.yml ├── init_test.go ├── main.go └── main_test.go ├── init_test.go ├── .gitignore ├── .travis.yml ├── env_var_builder.go ├── README.md ├── LICENSE ├── env_var_builder_test.go ├── parser.go ├── go.sum ├── docker_client.go ├── volume_mount_builder.go ├── docker_client_test.go ├── volume_mount_builder_test.go └── parser_test.go /fakes/docker/fail_run.go: -------------------------------------------------------------------------------- 1 | // +build fail_run 2 | 3 | package main 4 | 5 | func init() { 6 | failRun = true 7 | } 8 | -------------------------------------------------------------------------------- /fakes/docker/fail_pull.go: -------------------------------------------------------------------------------- 1 | // +build fail_pull 2 | 3 | package main 4 | 5 | func init() { 6 | failPull = true 7 | } 8 | -------------------------------------------------------------------------------- /fakes/docker/dockerconfig/constants.go: -------------------------------------------------------------------------------- 1 | package dockerconfig 2 | 3 | const InvocationsPath = "/tmp/piper/docker-invocations" 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/gopkg.in/yaml.v2"] 2 | path = vendor/gopkg.in/yaml.v2 3 | url = https://gopkg.in/yaml.v2.git 4 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ryanmoran/piper 2 | 3 | require ( 4 | github.com/onsi/ginkgo v1.8.0 5 | github.com/onsi/gomega v1.4.3 6 | gopkg.in/yaml.v2 v2.2.2 7 | ) 8 | -------------------------------------------------------------------------------- /piper/fixtures/task.yml: -------------------------------------------------------------------------------- 1 | --- 2 | image: docker:///my-image 3 | 4 | run: 5 | path: my-task.sh 6 | 7 | inputs: 8 | - name: input-1 9 | 10 | outputs: 11 | - name: output-1 12 | 13 | params: 14 | VAR1: default-var-1 15 | -------------------------------------------------------------------------------- /init_test.go: -------------------------------------------------------------------------------- 1 | package piper_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestPiper(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "piper") 13 | } 14 | -------------------------------------------------------------------------------- /piper/fixtures/advanced_task.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | run: 4 | path: my-task.sh 5 | 6 | image_resource: 7 | type: docker-image 8 | source: 9 | repository: my-image 10 | tag: 'x.y' 11 | 12 | inputs: 13 | - name: input 14 | path: some/path/input 15 | - name: optional-input 16 | optional: true 17 | 18 | outputs: 19 | - name: output 20 | path: some/path/output 21 | -------------------------------------------------------------------------------- /.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 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | binaries/ 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - "1.10" 4 | install: 5 | - go get -v ./... 6 | - go get github.com/onsi/gomega 7 | - go get github.com/onsi/ginkgo/ginkgo 8 | - export PATH=$PATH:$HOME/gopath/bin 9 | script: 10 | - GO15VENDOREXPERIMENT=1 $HOME/gopath/bin/ginkgo -r --randomizeAllSpecs --failOnPending 11 | --randomizeSuites --race 12 | - for OS in darwin linux; do GOOS=$OS GOARCH=amd64 go build -o binaries/piper-$OS piper/main.go; done; 13 | -------------------------------------------------------------------------------- /env_var_builder.go: -------------------------------------------------------------------------------- 1 | package piper 2 | 3 | import "strings" 4 | 5 | type EnvVarBuilder struct{} 6 | 7 | func (b EnvVarBuilder) Build(environment []string, params map[string]string) []DockerEnv { 8 | env := make(map[string]string) 9 | for _, variable := range environment { 10 | parts := strings.SplitN(variable, "=", 2) 11 | env[parts[0]] = parts[1] 12 | } 13 | 14 | var envVars []DockerEnv 15 | for key, value := range params { 16 | if env[key] != "" { 17 | value = env[key] 18 | } 19 | envVars = append(envVars, DockerEnv{ 20 | Key: key, 21 | Value: value, 22 | }) 23 | } 24 | 25 | return envVars 26 | } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/ryanmoran/piper.svg?branch=master)](https://travis-ci.org/ryanmoran/piper) 2 | 3 | # piper 4 | Like `fly execute` but running against a local docker daemon 5 | 6 | ## But why? 7 | Have you ever run `fly execute` with a set of huge inputs 8 | that take FOREVER to upload only to find out you had a typo 9 | in your task script? This is why. 10 | 11 | ## But how? 12 | `piper` is really just a wrapper around some calls to the 13 | `docker` cli. It pulls the image needed to run the task. 14 | It then runs `docker run` with some volume mounts for your 15 | inputs and outputs. Its as simple as that. 16 | 17 | ## Installation 18 | `go get github.com/ryanmoran/piper/piper` 19 | -------------------------------------------------------------------------------- /piper/init_test.go: -------------------------------------------------------------------------------- 1 | package main_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | "github.com/onsi/gomega/gexec" 11 | 12 | "testing" 13 | ) 14 | 15 | func TestPiperExecutable(t *testing.T) { 16 | RegisterFailHandler(Fail) 17 | RunSpecs(t, "piper/piper") 18 | } 19 | 20 | var ( 21 | pathToPiper string 22 | pathToDocker string 23 | ) 24 | 25 | var _ = BeforeSuite(func() { 26 | var err error 27 | pathToPiper, err = gexec.Build("github.com/ryanmoran/piper/piper") 28 | Expect(err).NotTo(HaveOccurred()) 29 | 30 | pathToDocker, err = gexec.Build("github.com/ryanmoran/piper/fakes/docker") 31 | Expect(err).NotTo(HaveOccurred()) 32 | 33 | os.Setenv("PATH", fmt.Sprintf("%s:%s", filepath.Dir(pathToDocker), os.Getenv("PATH"))) 34 | }) 35 | 36 | var _ = AfterSuite(func() { 37 | gexec.CleanupBuildArtifacts() 38 | }) 39 | -------------------------------------------------------------------------------- /fakes/docker/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/ryanmoran/piper/fakes/docker/dockerconfig" 10 | ) 11 | 12 | var failPull, failRun bool 13 | 14 | func main() { 15 | command := strings.Join(os.Args, " ") 16 | 17 | if failPull && strings.Contains(command, "docker pull") { 18 | log.Fatalln("failed to pull") 19 | } 20 | 21 | if failRun && strings.Contains(command, "docker run") { 22 | log.Fatalln("failed to run") 23 | } 24 | 25 | err := os.MkdirAll(filepath.Dir(dockerconfig.InvocationsPath), 0755) 26 | if err != nil { 27 | log.Fatalln(err) 28 | } 29 | 30 | invocations, err := os.OpenFile(dockerconfig.InvocationsPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) 31 | if err != nil { 32 | log.Fatalln(err) 33 | } 34 | 35 | _, err = invocations.WriteString(command + "\n") 36 | if err != nil { 37 | log.Fatalln(err) 38 | } 39 | 40 | err = invocations.Close() 41 | if err != nil { 42 | log.Fatalln(err) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Ryan Moran 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 | -------------------------------------------------------------------------------- /env_var_builder_test.go: -------------------------------------------------------------------------------- 1 | package piper_test 2 | 3 | import ( 4 | "github.com/ryanmoran/piper" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("EnvVarBuilder", func() { 11 | It("returns a list of environment variables", func() { 12 | vars := piper.EnvVarBuilder{}.Build([]string{ 13 | "VAR1=var-1", 14 | "VAR3=var-3", 15 | }, map[string]string{ 16 | "VAR1": "default-var-1", 17 | "VAR2": "default-var-2", 18 | }) 19 | Expect(vars).To(ConsistOf([]piper.DockerEnv{ 20 | { 21 | Key: "VAR1", 22 | Value: "var-1", 23 | }, 24 | { 25 | Key: "VAR2", 26 | Value: "default-var-2", 27 | }, 28 | })) 29 | 30 | }) 31 | 32 | Context("when env vars have '=' signs in the value", func() { 33 | It("returns a list of environment variables with '=' signs still in their place", func() { 34 | vars := piper.EnvVarBuilder{}.Build([]string{ 35 | "VAR1=var-1==42=", 36 | }, map[string]string{ 37 | "VAR1": "meow", 38 | }) 39 | Expect(vars).To(ConsistOf([]piper.DockerEnv{ 40 | { 41 | Key: "VAR1", 42 | Value: "var-1==42=", 43 | }, 44 | })) 45 | }) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /parser.go: -------------------------------------------------------------------------------- 1 | package piper 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "strings" 7 | 8 | "gopkg.in/yaml.v2" 9 | ) 10 | 11 | type VolumeMount struct { 12 | Name string `yaml:"name"` 13 | Path string `yaml:"path"` 14 | Optional bool `yaml:"optional"` 15 | } 16 | 17 | type Run struct { 18 | Path string `yaml:"path"` 19 | Args []string `yaml:"args"` 20 | dir string `yaml:"dir"` 21 | } 22 | 23 | type ImageResourceSource struct { 24 | Repository string 25 | Tag string 26 | } 27 | 28 | func (i ImageResourceSource) String() string { 29 | if "" != i.Tag { 30 | return fmt.Sprintf("%s:%s", i.Repository, i.Tag) 31 | } 32 | return i.Repository 33 | } 34 | 35 | type ImageResource struct { 36 | Source ImageResourceSource 37 | } 38 | 39 | type Task struct { 40 | Image string `yaml:"image"` 41 | Run Run 42 | Inputs []VolumeMount 43 | Outputs []VolumeMount 44 | Caches []VolumeMount 45 | Params map[string]string 46 | ImageResource ImageResource `yaml:"image_resource"` 47 | } 48 | 49 | type Parser struct{} 50 | 51 | func (p Parser) Parse(path string) (Task, error) { 52 | contents, err := ioutil.ReadFile(path) 53 | if err != nil { 54 | return Task{}, err 55 | } 56 | 57 | var task Task 58 | 59 | err = yaml.Unmarshal(contents, &task) 60 | if err != nil { 61 | return Task{}, err 62 | } 63 | if task.ImageResource.Source.Repository != "" { 64 | task.Image = task.ImageResource.Source.String() 65 | } else { 66 | task.Image = strings.TrimPrefix(task.Image, "docker:///") 67 | } 68 | 69 | return task, nil 70 | } 71 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 2 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 3 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 4 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 5 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 6 | github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= 7 | github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 8 | github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= 9 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 10 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= 11 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 12 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 13 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs= 14 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 15 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 16 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 17 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 18 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 19 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 20 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 21 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 22 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 23 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 24 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 25 | -------------------------------------------------------------------------------- /docker_client.go: -------------------------------------------------------------------------------- 1 | package piper 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os/exec" 7 | "strings" 8 | ) 9 | 10 | type DockerVolumeMount struct { 11 | LocalPath string 12 | RemotePath string 13 | } 14 | 15 | func (m DockerVolumeMount) String() string { 16 | return fmt.Sprintf("--volume=%s:%s", m.LocalPath, m.RemotePath) 17 | } 18 | 19 | type DockerEnv struct { 20 | Key string 21 | Value string 22 | } 23 | 24 | func (e DockerEnv) String() string { 25 | return fmt.Sprintf("--env=%s=%s", e.Key, e.Value) 26 | } 27 | 28 | type DockerClient struct { 29 | Command *exec.Cmd 30 | Stdout io.Writer 31 | Stderr io.Writer 32 | } 33 | 34 | func (c DockerClient) Pull(image string, dryRun bool) error { 35 | args := append(c.Command.Args, "pull", image) 36 | 37 | if dryRun { 38 | fmt.Fprintln(c.Stdout, strings.Join(args, " ")) 39 | return nil 40 | } 41 | 42 | c.Command.Args = args 43 | c.Command.Stdout = c.Stdout 44 | c.Command.Stderr = c.Stderr 45 | 46 | err := c.Command.Run() 47 | if err != nil { 48 | return err 49 | } 50 | 51 | return nil 52 | } 53 | 54 | func (c DockerClient) Run( 55 | command []string, 56 | image string, 57 | envVars []DockerEnv, 58 | mounts []DockerVolumeMount, 59 | privileged bool, 60 | dryRun bool, 61 | rm bool, 62 | ) error { 63 | c.Command.Args = append(c.Command.Args, "run", fmt.Sprintf("--workdir=%s", VolumeMountPoint)) 64 | 65 | if privileged { 66 | c.Command.Args = append(c.Command.Args, "--privileged") 67 | } 68 | 69 | if rm { 70 | c.Command.Args = append(c.Command.Args, "--rm") 71 | } 72 | 73 | for _, envVar := range envVars { 74 | c.Command.Args = append(c.Command.Args, envVar.String()) 75 | } 76 | 77 | for _, mount := range mounts { 78 | c.Command.Args = append(c.Command.Args, mount.String()) 79 | } 80 | 81 | c.Command.Args = append(c.Command.Args, "--tty") 82 | c.Command.Args = append(c.Command.Args, image) 83 | c.Command.Args = append(c.Command.Args, command...) 84 | 85 | if dryRun { 86 | fmt.Fprintln(c.Stdout, strings.Join(c.Command.Args, " ")) 87 | return nil 88 | } 89 | 90 | c.Command.Stdout = c.Stdout 91 | c.Command.Stderr = c.Stderr 92 | 93 | err := c.Command.Run() 94 | if err != nil { 95 | return err 96 | } 97 | 98 | return nil 99 | } 100 | -------------------------------------------------------------------------------- /volume_mount_builder.go: -------------------------------------------------------------------------------- 1 | package piper 2 | 3 | import ( 4 | "fmt" 5 | "os/user" 6 | "path/filepath" 7 | "strings" 8 | ) 9 | 10 | const VolumeMountPoint = "/tmp/build" 11 | 12 | type VolumeMountBuilder struct{} 13 | 14 | func (b VolumeMountBuilder) Build(resources []VolumeMount, inputs, outputs []string) ([]DockerVolumeMount, error) { 15 | pairsMap := make(map[string]string) 16 | 17 | for _, input := range inputs { 18 | parts := strings.Split(input, "=") 19 | if len(parts) != 2 { 20 | return nil, fmt.Errorf("could not parse input %q. must be of form =", input) 21 | } 22 | 23 | expandedPath, err := expandUser(parts[1]) 24 | if err != nil { 25 | return nil, err 26 | } 27 | pairsMap[parts[0]] = expandedPath 28 | } 29 | 30 | for _, output := range outputs { 31 | parts := strings.Split(output, "=") 32 | if len(parts) != 2 { 33 | return nil, fmt.Errorf("could not parse output %q. must be of form =", output) 34 | } 35 | 36 | expandedPath, err := expandUser(parts[1]) 37 | if err != nil { 38 | return nil, err 39 | } 40 | pairsMap[parts[0]] = expandedPath 41 | } 42 | 43 | var mounts []DockerVolumeMount 44 | var missingResources []string 45 | for _, resource := range resources { 46 | if resource.Name == "" && resource.Path != "" { 47 | mountPoint := filepath.Join(VolumeMountPoint, resource.Path) 48 | 49 | mounts = append(mounts, DockerVolumeMount{ 50 | LocalPath: "/tmp", 51 | RemotePath: filepath.Clean(mountPoint), 52 | }) 53 | continue 54 | } 55 | 56 | resourceLocation, ok := pairsMap[resource.Name] 57 | if !ok { 58 | if !resource.Optional { 59 | missingResources = append(missingResources, resource.Name) 60 | } 61 | continue 62 | } 63 | var mountPoint string 64 | if resource.Path == "" { 65 | mountPoint = filepath.Join(VolumeMountPoint, resource.Name) 66 | } else { 67 | mountPoint = filepath.Join(VolumeMountPoint, resource.Path) 68 | } 69 | 70 | mounts = append(mounts, DockerVolumeMount{ 71 | LocalPath: resourceLocation, 72 | RemotePath: filepath.Clean(mountPoint), 73 | }) 74 | } 75 | if len(missingResources) != 0 { 76 | return nil, fmt.Errorf("The following required inputs/outputs are not satisfied: %s.", strings.Join(missingResources, ", ")) 77 | } 78 | 79 | return mounts, nil 80 | } 81 | 82 | func expandUser(path string) (string, error) { 83 | if !strings.HasPrefix(path, "~/") { 84 | return path, nil 85 | } 86 | 87 | usr, err := user.Current() 88 | if err != nil { 89 | return "", err 90 | } 91 | dir := usr.HomeDir 92 | return filepath.Join(dir, path[2:]), nil 93 | } 94 | -------------------------------------------------------------------------------- /piper/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/exec" 9 | 10 | "github.com/ryanmoran/piper" 11 | ) 12 | 13 | func main() { 14 | var ( 15 | taskFilePath string 16 | inputPairs ResourcePairs 17 | outputPairs ResourcePairs 18 | privileged bool 19 | dryRun bool 20 | rm bool 21 | repository string 22 | tag string 23 | ) 24 | 25 | flag.StringVar(&taskFilePath, "c", "", "path to the task configuration file") 26 | flag.Var(&inputPairs, "i", "=") 27 | flag.Var(&outputPairs, "o", "=") 28 | flag.BoolVar(&privileged, "p", false, "run the task with full privileges") 29 | flag.BoolVar(&dryRun, "dry-run", false, "prints the docker commands without running them") 30 | flag.BoolVar(&rm, "rm", false, "removes the docker container after test") 31 | flag.StringVar(&repository, "r", "", "docker image repo") 32 | flag.StringVar(&tag, "t", "", "image tag") 33 | 34 | flag.Parse() 35 | 36 | var errors []string 37 | if len(taskFilePath) == 0 { 38 | errors = append(errors, fmt.Sprintf(" -c is a required flag")) 39 | } 40 | 41 | if len(errors) > 0 { 42 | fmt.Fprintln(os.Stderr, "Errors:") 43 | for _, err := range errors { 44 | fmt.Fprintln(os.Stderr, err) 45 | } 46 | fmt.Fprintln(os.Stderr, "\nUsage:") 47 | flag.PrintDefaults() 48 | os.Exit(1) 49 | } 50 | 51 | taskConfig, err := piper.Parser{}.Parse(taskFilePath) 52 | if err != nil { 53 | log.Fatalln(err) 54 | } 55 | 56 | var resources []piper.VolumeMount 57 | resources = append(resources, taskConfig.Inputs...) 58 | resources = append(resources, taskConfig.Outputs...) 59 | resources = append(resources, taskConfig.Caches...) 60 | 61 | volumeMounts, err := piper.VolumeMountBuilder{}.Build(resources, inputPairs, outputPairs) 62 | if err != nil { 63 | log.Fatalln(err) 64 | } 65 | 66 | envVars := piper.EnvVarBuilder{}.Build(os.Environ(), taskConfig.Params) 67 | 68 | dockerPath, err := exec.LookPath("docker") 69 | if err != nil { 70 | log.Fatalln(err) 71 | } 72 | 73 | dockerClient := piper.DockerClient{ 74 | Command: exec.Command(dockerPath), 75 | Stdout: os.Stdout, 76 | Stderr: os.Stderr, 77 | } 78 | 79 | dockerRepo := taskConfig.Image 80 | if len(repository) > 0 { 81 | dockerRepo = repository 82 | } 83 | if len(tag) > 0 { 84 | dockerRepo += fmt.Sprintf(":%s", tag) 85 | } 86 | 87 | err = dockerClient.Pull(dockerRepo, dryRun) 88 | if err != nil { 89 | log.Fatalln(err) 90 | } 91 | 92 | dockerClient = piper.DockerClient{ 93 | Command: exec.Command(dockerPath), 94 | Stdout: os.Stdout, 95 | Stderr: os.Stderr, 96 | } 97 | 98 | command := []string{taskConfig.Run.Path} 99 | command = append(command, taskConfig.Run.Args...) 100 | 101 | err = dockerClient.Run(command, dockerRepo, envVars, volumeMounts, privileged, dryRun, rm) 102 | if err != nil { 103 | log.Fatalln(err) 104 | } 105 | } 106 | 107 | type ResourcePairs []string 108 | 109 | func (p *ResourcePairs) Set(resource string) error { 110 | *p = append(*p, resource) 111 | return nil 112 | } 113 | 114 | func (p *ResourcePairs) String() string { 115 | return fmt.Sprint(*p) 116 | } 117 | -------------------------------------------------------------------------------- /docker_client_test.go: -------------------------------------------------------------------------------- 1 | package piper_test 2 | 3 | import ( 4 | "bytes" 5 | "os/exec" 6 | "strings" 7 | 8 | "github.com/ryanmoran/piper" 9 | 10 | . "github.com/onsi/ginkgo" 11 | . "github.com/onsi/gomega" 12 | ) 13 | 14 | var _ = Describe("DockerClient", func() { 15 | var ( 16 | client piper.DockerClient 17 | stdout *bytes.Buffer 18 | ) 19 | 20 | BeforeEach(func() { 21 | stdout = bytes.NewBuffer([]byte{}) 22 | command := exec.Command("echo") 23 | 24 | client = piper.DockerClient{ 25 | Command: command, 26 | Stdout: stdout, 27 | } 28 | }) 29 | 30 | Describe("Pull", func() { 31 | It("pulls the specified docker image", func() { 32 | err := client.Pull("some-image", false) 33 | Expect(err).NotTo(HaveOccurred()) 34 | 35 | Expect(stdout.String()).To(Equal("pull some-image\n")) 36 | }) 37 | 38 | It("prints the docker command without running it", func() { 39 | err := client.Pull("some-image", true) 40 | Expect(err).NotTo(HaveOccurred()) 41 | 42 | Expect(stdout.String()).To(Equal("echo pull some-image\n")) 43 | }) 44 | 45 | Context("failure cases", func() { 46 | Context("when the executable cannot be found", func() { 47 | It("returns an error", func() { 48 | client = piper.DockerClient{ 49 | Command: exec.Command("no-such-executable"), 50 | Stdout: stdout, 51 | } 52 | err := client.Pull("some-image", false) 53 | Expect(err).To(MatchError(ContainSubstring("executable file not found in $PATH"))) 54 | }) 55 | }) 56 | }) 57 | }) 58 | 59 | Describe("Run", func() { 60 | It("runs the command with the given volume mounts, and environment", func() { 61 | err := client.Run([]string{"my-task.sh", "-my-arg1", "-my-arg2"}, "my-image", []piper.DockerEnv{ 62 | { 63 | Key: "VAR1", 64 | Value: "var-1", 65 | }, 66 | { 67 | Key: "VAR2", 68 | Value: "var-2", 69 | }, 70 | }, []piper.DockerVolumeMount{ 71 | { 72 | LocalPath: "/some/local/path-1", 73 | RemotePath: "/some/remote/path-1", 74 | }, 75 | { 76 | LocalPath: "/some/local/path-2", 77 | RemotePath: "/some/remote/path-2", 78 | }, 79 | }, false, false, false) 80 | Expect(err).NotTo(HaveOccurred()) 81 | 82 | args := []string{ 83 | "run", 84 | "--workdir=/tmp/build", 85 | "--env=VAR1=var-1", 86 | "--env=VAR2=var-2", 87 | "--volume=/some/local/path-1:/some/remote/path-1", 88 | "--volume=/some/local/path-2:/some/remote/path-2", 89 | "--tty", 90 | "my-image", 91 | "my-task.sh", 92 | "-my-arg1", 93 | "-my-arg2", 94 | } 95 | 96 | Expect(stdout.String()).To(Equal(strings.Join(args, " ") + "\n")) 97 | }) 98 | 99 | It("runs the command in privileged mode", func() { 100 | err := client.Run([]string{"my-task.sh"}, "my-image", 101 | []piper.DockerEnv{}, 102 | []piper.DockerVolumeMount{}, true, false, false) 103 | Expect(err).NotTo(HaveOccurred()) 104 | 105 | args := []string{ 106 | "run", 107 | "--workdir=/tmp/build", 108 | "--privileged", 109 | "--tty", 110 | "my-image", 111 | "my-task.sh", 112 | } 113 | 114 | Expect(stdout.String()).To(Equal(strings.Join(args, " ") + "\n")) 115 | }) 116 | 117 | It("runs the command with --rm argument", func() { 118 | err := client.Run([]string{"my-task.sh"}, "my-image", 119 | []piper.DockerEnv{}, 120 | []piper.DockerVolumeMount{}, false, false, true) 121 | Expect(err).NotTo(HaveOccurred()) 122 | 123 | args := []string{ 124 | "run", 125 | "--workdir=/tmp/build", 126 | "--rm", 127 | "--tty", 128 | "my-image", 129 | "my-task.sh", 130 | } 131 | 132 | Expect(stdout.String()).To(Equal(strings.Join(args, " ") + "\n")) 133 | }) 134 | 135 | It("prints the docker command without running it", func() { 136 | err := client.Run([]string{"my-task.sh"}, "my-image", 137 | []piper.DockerEnv{}, 138 | []piper.DockerVolumeMount{}, true, true, false) 139 | Expect(err).NotTo(HaveOccurred()) 140 | 141 | args := []string{ 142 | "echo", 143 | "run", 144 | "--workdir=/tmp/build", 145 | "--privileged", 146 | "--tty", 147 | "my-image", 148 | "my-task.sh", 149 | } 150 | 151 | Expect(stdout.String()).To(Equal(strings.Join(args, " ") + "\n")) 152 | }) 153 | 154 | Context("failure cases", func() { 155 | Context("when the executable cannot be found", func() { 156 | It("returns an error", func() { 157 | client = piper.DockerClient{ 158 | Command: exec.Command("no-such-executable"), 159 | Stdout: stdout, 160 | } 161 | err := client.Run([]string{"some-command"}, "some-image", []piper.DockerEnv{}, []piper.DockerVolumeMount{}, false, false, false) 162 | Expect(err).To(MatchError(ContainSubstring("executable file not found in $PATH"))) 163 | }) 164 | }) 165 | }) 166 | }) 167 | }) 168 | -------------------------------------------------------------------------------- /volume_mount_builder_test.go: -------------------------------------------------------------------------------- 1 | package piper_test 2 | 3 | import ( 4 | "github.com/ryanmoran/piper" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("VolumeMountBuilder", func() { 11 | var builder piper.VolumeMountBuilder 12 | 13 | Describe("Build", func() { 14 | It("builds the volume mounts", func() { 15 | mounts, err := builder.Build([]piper.VolumeMount{ 16 | piper.VolumeMount{Name: "input-1"}, 17 | piper.VolumeMount{Name: "input-2", Optional: true}, 18 | piper.VolumeMount{Name: "output-1"}, 19 | piper.VolumeMount{Name: "output-2"}, 20 | piper.VolumeMount{Path: "cache-1"}, 21 | }, []string{ 22 | "input-1=/some/path-1", 23 | "input-2=/some/path-2", 24 | }, []string{ 25 | "output-1=/some/path-3", 26 | "output-2=/some/path-4", 27 | }) 28 | Expect(err).NotTo(HaveOccurred()) 29 | Expect(mounts[0:4]).To(Equal([]piper.DockerVolumeMount{ 30 | { 31 | LocalPath: "/some/path-1", 32 | RemotePath: "/tmp/build/input-1", 33 | }, 34 | { 35 | LocalPath: "/some/path-2", 36 | RemotePath: "/tmp/build/input-2", 37 | }, 38 | { 39 | LocalPath: "/some/path-3", 40 | RemotePath: "/tmp/build/output-1", 41 | }, 42 | { 43 | LocalPath: "/some/path-4", 44 | RemotePath: "/tmp/build/output-2", 45 | }, 46 | })) 47 | Expect(mounts[4].LocalPath).To(Equal("/tmp")) 48 | Expect(mounts[4].RemotePath).To(Equal("/tmp/build/cache-1")) 49 | }) 50 | 51 | It("expands '~' in paths", func() { 52 | mounts, err := builder.Build([]piper.VolumeMount{ 53 | piper.VolumeMount{Name: "input-1"}, 54 | piper.VolumeMount{Name: "output-1"}, 55 | }, []string{ 56 | "input-1=~/some/path-1", 57 | }, []string{ 58 | "output-1=~/some/path-2", 59 | }) 60 | Expect(err).ToNot(HaveOccurred()) 61 | Expect(mounts[0].LocalPath).ShouldNot(ContainSubstring("~")) 62 | Expect(mounts[1].LocalPath).ShouldNot(ContainSubstring("~")) 63 | }) 64 | 65 | It("honors the path given in the VolumeMount", func() { 66 | mounts, err := builder.Build([]piper.VolumeMount{ 67 | piper.VolumeMount{Name: "input-1", Path: "some/path/to/input"}, 68 | piper.VolumeMount{Name: "input-2"}, 69 | piper.VolumeMount{Name: "output-1"}, 70 | piper.VolumeMount{Name: "output-2", Path: "some/path/to/output"}, 71 | }, []string{ 72 | "input-1=/some/path-1", 73 | "input-2=/some/path-2", 74 | }, []string{ 75 | "output-1=/some/path-3", 76 | "output-2=/some/path-4", 77 | }) 78 | Expect(err).NotTo(HaveOccurred()) 79 | Expect(mounts).To(Equal([]piper.DockerVolumeMount{ 80 | { 81 | LocalPath: "/some/path-1", 82 | RemotePath: "/tmp/build/some/path/to/input", 83 | }, 84 | { 85 | LocalPath: "/some/path-2", 86 | RemotePath: "/tmp/build/input-2", 87 | }, 88 | { 89 | LocalPath: "/some/path-3", 90 | RemotePath: "/tmp/build/output-1", 91 | }, 92 | { 93 | LocalPath: "/some/path-4", 94 | RemotePath: "/tmp/build/some/path/to/output", 95 | }, 96 | })) 97 | }) 98 | 99 | Context("failure cases", func() { 100 | Context("when the input pairs are malformed", func() { 101 | It("returns an error", func() { 102 | _, err := builder.Build([]piper.VolumeMount{}, []string{ 103 | "input-1=something", 104 | "input-2", 105 | }, []string{}) 106 | Expect(err).To(MatchError("could not parse input \"input-2\". must be of form =")) 107 | }) 108 | }) 109 | 110 | Context("when an input pair is not specified, but is required", func() { 111 | It("returns an error", func() { 112 | _, err := builder.Build([]piper.VolumeMount{ 113 | {Name: "input-1"}, 114 | {Name: "input-2"}, 115 | {Name: "input-3"}, 116 | }, []string{ 117 | "input-1=/some/path-1", 118 | }, []string{}) 119 | Expect(err).To(MatchError(`The following required inputs/outputs are not satisfied: input-2, input-3.`)) 120 | }) 121 | }) 122 | 123 | Context("when the output pairs are malformed", func() { 124 | It("returns an error", func() { 125 | _, err := builder.Build([]piper.VolumeMount{}, []string{}, []string{ 126 | "output-1=something", 127 | "output-2", 128 | }) 129 | Expect(err).To(MatchError("could not parse output \"output-2\". must be of form =")) 130 | }) 131 | }) 132 | 133 | Context("when an input pair is not specified, but is required", func() { 134 | It("returns an error", func() { 135 | _, err := builder.Build([]piper.VolumeMount{ 136 | {Name: "output-1"}, 137 | {Name: "output-2"}, 138 | {Name: "output-3"}, 139 | }, []string{}, []string{ 140 | "output-1=/some/path-1", 141 | }) 142 | Expect(err).To(MatchError(`The following required inputs/outputs are not satisfied: output-2, output-3.`)) 143 | }) 144 | }) 145 | }) 146 | }) 147 | }) 148 | -------------------------------------------------------------------------------- /parser_test.go: -------------------------------------------------------------------------------- 1 | package piper_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | 7 | "github.com/ryanmoran/piper" 8 | 9 | . "github.com/onsi/ginkgo" 10 | . "github.com/onsi/gomega" 11 | ) 12 | 13 | var _ = Describe("Parser", func() { 14 | Describe("Parse", func() { 15 | var ( 16 | configFilePath string 17 | parser piper.Parser 18 | ) 19 | 20 | BeforeEach(func() { 21 | tempFile, err := ioutil.TempFile("", "") 22 | Expect(err).NotTo(HaveOccurred()) 23 | 24 | _, err = tempFile.WriteString(`--- 25 | image: docker:///some-docker-image 26 | run: 27 | path: /path/to/run/command 28 | args: ['-arg1', '-arg2'] 29 | inputs: 30 | - name: input-1 31 | - name: input-2 32 | - name: input-3 33 | outputs: 34 | - name: output-1 35 | - name: output-2 36 | - name: output-3 37 | caches: 38 | - path: cache-1 39 | - path: cache-2 40 | - path: cache-3 41 | params: 42 | VAR1: var-1 43 | VAR2: var-2 44 | `) 45 | Expect(err).NotTo(HaveOccurred()) 46 | 47 | configFilePath = tempFile.Name() 48 | 49 | err = tempFile.Close() 50 | Expect(err).NotTo(HaveOccurred()) 51 | }) 52 | 53 | AfterEach(func() { 54 | err := os.RemoveAll(configFilePath) 55 | Expect(err).NotTo(HaveOccurred()) 56 | }) 57 | 58 | It("parses the task config for the docker image", func() { 59 | config, err := parser.Parse(configFilePath) 60 | Expect(err).NotTo(HaveOccurred()) 61 | 62 | Expect(config.Image).To(Equal("some-docker-image")) 63 | }) 64 | 65 | It("parses the task config for the run command", func() { 66 | config, err := parser.Parse(configFilePath) 67 | Expect(err).NotTo(HaveOccurred()) 68 | 69 | Expect(config.Run.Path).To(Equal("/path/to/run/command")) 70 | Expect(config.Run.Args).To(Equal([]string{"-arg1", "-arg2"})) 71 | }) 72 | 73 | It("parses the task config for the inputs", func() { 74 | config, err := parser.Parse(configFilePath) 75 | Expect(err).NotTo(HaveOccurred()) 76 | 77 | Expect(config.Inputs).To(Equal([]piper.VolumeMount{ 78 | {Name: "input-1"}, 79 | {Name: "input-2"}, 80 | {Name: "input-3"}, 81 | })) 82 | }) 83 | 84 | It("parses the task config for the caches", func() { 85 | config, err := parser.Parse(configFilePath) 86 | Expect(err).NotTo(HaveOccurred()) 87 | 88 | Expect(config.Caches).To(Equal([]piper.VolumeMount{ 89 | {Path: "cache-1"}, 90 | {Path: "cache-2"}, 91 | {Path: "cache-3"}, 92 | })) 93 | }) 94 | 95 | It("parses the task config for the outputs", func() { 96 | config, err := parser.Parse(configFilePath) 97 | Expect(err).NotTo(HaveOccurred()) 98 | 99 | Expect(config.Outputs).To(Equal([]piper.VolumeMount{ 100 | {Name: "output-1"}, 101 | {Name: "output-2"}, 102 | {Name: "output-3"}, 103 | })) 104 | }) 105 | 106 | It("parses the task config for the params", func() { 107 | config, err := parser.Parse(configFilePath) 108 | Expect(err).NotTo(HaveOccurred()) 109 | 110 | Expect(config.Params).To(Equal(map[string]string{ 111 | "VAR1": "var-1", 112 | "VAR2": "var-2", 113 | })) 114 | }) 115 | 116 | It("honors the image_resource", func() { 117 | tempFile, err := ioutil.TempFile("", "") 118 | Expect(err).NotTo(HaveOccurred()) 119 | _, err = tempFile.WriteString(`--- 120 | image_resource: 121 | type: docker-image 122 | source: 123 | repository: repo/docker-image-name 124 | run: 125 | path: /path/to/run/command 126 | inputs: 127 | - name: input-1 128 | - name: input-2 129 | - name: input-3 130 | params: 131 | VAR1: var-1 132 | VAR2: var-2 133 | `) 134 | Expect(err).NotTo(HaveOccurred()) 135 | 136 | configFilePath = tempFile.Name() 137 | 138 | err = tempFile.Close() 139 | Expect(err).NotTo(HaveOccurred()) 140 | config, err := parser.Parse(configFilePath) 141 | Expect(err).NotTo(HaveOccurred()) 142 | 143 | Expect(config.Image).To(Equal("repo/docker-image-name")) 144 | }) 145 | 146 | It("honors the image_resource with tags", func() { 147 | tempFile, err := ioutil.TempFile("", "") 148 | Expect(err).NotTo(HaveOccurred()) 149 | _, err = tempFile.WriteString(`--- 150 | image_resource: 151 | type: docker-image 152 | source: 153 | repository: repo/docker-image-name 154 | tag: '1.7' 155 | run: 156 | path: /path/to/run/command 157 | inputs: 158 | - name: input-1 159 | - name: input-2 160 | - name: input-3 161 | params: 162 | VAR1: var-1 163 | VAR2: var-2 164 | `) 165 | Expect(err).NotTo(HaveOccurred()) 166 | 167 | configFilePath = tempFile.Name() 168 | 169 | err = tempFile.Close() 170 | Expect(err).NotTo(HaveOccurred()) 171 | config, err := parser.Parse(configFilePath) 172 | Expect(err).NotTo(HaveOccurred()) 173 | 174 | Expect(config.Image).To(Equal("repo/docker-image-name:1.7")) 175 | }) 176 | 177 | Context("failure cases", func() { 178 | Context("when the task file does not exist", func() { 179 | It("returns an error", func() { 180 | err := os.RemoveAll(configFilePath) 181 | Expect(err).NotTo(HaveOccurred()) 182 | 183 | _, err = parser.Parse(configFilePath) 184 | Expect(err).To(MatchError(ContainSubstring("no such file or directory"))) 185 | }) 186 | }) 187 | 188 | Context("when the task file yaml is not valid", func() { 189 | It("returns an error", func() { 190 | err := ioutil.WriteFile(configFilePath, []byte("%%%%%"), 0644) 191 | Expect(err).NotTo(HaveOccurred()) 192 | 193 | _, err = parser.Parse(configFilePath) 194 | Expect(err).To(MatchError(ContainSubstring("could not find expected directive name"))) 195 | }) 196 | }) 197 | }) 198 | }) 199 | }) 200 | -------------------------------------------------------------------------------- /piper/main_test.go: -------------------------------------------------------------------------------- 1 | package main_test 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/onsi/gomega/gexec" 12 | "github.com/ryanmoran/piper/fakes/docker/dockerconfig" 13 | 14 | . "github.com/onsi/ginkgo" 15 | . "github.com/onsi/gomega" 16 | ) 17 | 18 | var _ = Describe("Piper", func() { 19 | BeforeEach(func() { 20 | err := os.RemoveAll(dockerconfig.InvocationsPath) 21 | Expect(err).NotTo(HaveOccurred()) 22 | }) 23 | 24 | It("runs a concourse task", func() { 25 | command := exec.Command(pathToPiper, 26 | "-c", "fixtures/task.yml", 27 | "-i", "input-1=/tmp/local-1", 28 | "-o", "output-1=/tmp/local-2", 29 | ) 30 | command.Env = append(os.Environ(), "VAR1=var-1") 31 | 32 | session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) 33 | Expect(err).NotTo(HaveOccurred()) 34 | 35 | Eventually(session).Should(gexec.Exit(0)) 36 | 37 | dockerInvocations, err := ioutil.ReadFile(dockerconfig.InvocationsPath) 38 | Expect(err).NotTo(HaveOccurred()) 39 | 40 | dockerCommands := strings.Split(strings.TrimSpace(string(dockerInvocations)), "\n") 41 | Expect(dockerCommands).To(Equal([]string{ 42 | fmt.Sprintf("%s pull my-image", pathToDocker), 43 | fmt.Sprintf("%s run --workdir=/tmp/build --env=VAR1=var-1 --volume=/tmp/local-1:/tmp/build/input-1 --volume=/tmp/local-2:/tmp/build/output-1 --tty my-image my-task.sh", pathToDocker), 44 | })) 45 | }) 46 | 47 | It("runs a concourse task with input image and tag", func() { 48 | command := exec.Command(pathToPiper, 49 | "-c", "fixtures/task.yml", 50 | "-r", "my-image", 51 | "-t", "my-tag", 52 | "-i", "input-1=/tmp/local-1", 53 | "-o", "output-1=/tmp/local-2", 54 | ) 55 | command.Env = append(os.Environ(), "VAR1=var-1") 56 | 57 | session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) 58 | Expect(err).NotTo(HaveOccurred()) 59 | 60 | Eventually(session).Should(gexec.Exit(0)) 61 | 62 | dockerInvocations, err := ioutil.ReadFile(dockerconfig.InvocationsPath) 63 | Expect(err).NotTo(HaveOccurred()) 64 | 65 | dockerCommands := strings.Split(strings.TrimSpace(string(dockerInvocations)), "\n") 66 | Expect(dockerCommands).To(Equal([]string{ 67 | fmt.Sprintf("%s pull my-image:my-tag", pathToDocker), 68 | fmt.Sprintf("%s run --workdir=/tmp/build --env=VAR1=var-1 --volume=/tmp/local-1:/tmp/build/input-1 --volume=/tmp/local-2:/tmp/build/output-1 --tty my-image:my-tag my-task.sh", pathToDocker), 69 | })) 70 | }) 71 | 72 | It("runs a concourse task with complex inputs", func() { 73 | command := exec.Command(pathToPiper, 74 | "-c", "fixtures/advanced_task.yml", 75 | "-i", "input=/tmp/local-1", 76 | "-o", "output=/tmp/local-2", 77 | "-p") 78 | session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) 79 | Expect(err).NotTo(HaveOccurred()) 80 | 81 | Eventually(session).Should(gexec.Exit(0)) 82 | 83 | dockerInvocations, err := ioutil.ReadFile(dockerconfig.InvocationsPath) 84 | Expect(err).NotTo(HaveOccurred()) 85 | 86 | dockerCommands := strings.Split(strings.TrimSpace(string(dockerInvocations)), "\n") 87 | Expect(dockerCommands).To(Equal([]string{ 88 | fmt.Sprintf("%s pull my-image:x.y", pathToDocker), 89 | fmt.Sprintf("%s run --workdir=/tmp/build --privileged --volume=/tmp/local-1:/tmp/build/some/path/input --volume=/tmp/local-2:/tmp/build/some/path/output --tty my-image:x.y my-task.sh", pathToDocker), 90 | })) 91 | }) 92 | 93 | It("prints the docker commands to stdout, but does not execute them", func() { 94 | command := exec.Command(pathToPiper, 95 | "--dry-run", 96 | "-c", "fixtures/advanced_task.yml", 97 | "-i", "input=/tmp/local-1", 98 | "-o", "output=/tmp/local-2", 99 | "-p") 100 | session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) 101 | Expect(err).NotTo(HaveOccurred()) 102 | 103 | Eventually(session).Should(gexec.Exit(0)) 104 | 105 | dockerCommands := strings.Split(strings.TrimSpace(string(session.Out.Contents())), "\n") 106 | Expect(dockerCommands).To(Equal([]string{ 107 | fmt.Sprintf("%s pull my-image:x.y", pathToDocker), 108 | fmt.Sprintf("%s run --workdir=/tmp/build --privileged --volume=/tmp/local-1:/tmp/build/some/path/input --volume=/tmp/local-2:/tmp/build/some/path/output --tty my-image:x.y my-task.sh", pathToDocker), 109 | })) 110 | _, err = os.Stat(dockerconfig.InvocationsPath) 111 | Expect(os.IsNotExist(err)).To(BeTrue()) 112 | }) 113 | 114 | Context("failure cases", func() { 115 | Context("when the flag is not passed in", func() { 116 | It("Print an error and exit with status 1", func() { 117 | command := exec.Command(pathToPiper) 118 | session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) 119 | Expect(err).NotTo(HaveOccurred()) 120 | 121 | Eventually(session).Should(gexec.Exit(1)) 122 | Expect(session.Err.Contents()).To(ContainSubstring("-c is a required flag")) 123 | }) 124 | }) 125 | 126 | Context("when the task file does not exist", func() { 127 | It("prints an error and exits 1", func() { 128 | command := exec.Command(pathToPiper, "-c", "no-such-file") 129 | session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) 130 | Expect(err).NotTo(HaveOccurred()) 131 | 132 | Eventually(session).Should(gexec.Exit(1)) 133 | Expect(session.Err.Contents()).To(ContainSubstring("no such file or directory")) 134 | }) 135 | }) 136 | 137 | Context("when inputs are missing", func() { 138 | It("prints an error and exits 1", func() { 139 | command := exec.Command(pathToPiper, "-c", "fixtures/task.yml") 140 | session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) 141 | Expect(err).NotTo(HaveOccurred()) 142 | 143 | Eventually(session).Should(gexec.Exit(1)) 144 | Expect(session.Err.Contents()).To(ContainSubstring("The following required inputs/outputs are not satisfied: input-1, output-1.")) 145 | }) 146 | }) 147 | 148 | Context("when docker cannot be found on the $PATH", func() { 149 | var path string 150 | 151 | BeforeEach(func() { 152 | path = os.Getenv("PATH") 153 | os.Setenv("PATH", "") 154 | }) 155 | 156 | AfterEach(func() { 157 | os.Setenv("PATH", path) 158 | }) 159 | 160 | It("prints an error and exits 1", func() { 161 | command := exec.Command(pathToPiper, 162 | "-c", "fixtures/task.yml", 163 | "-i", "input-1=/tmp/local-1", 164 | "-o", "output-1=/tmp/local-2") 165 | session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) 166 | Expect(err).NotTo(HaveOccurred()) 167 | 168 | Eventually(session).Should(gexec.Exit(1)) 169 | Expect(session.Err.Contents()).To(ContainSubstring("executable file not found in $PATH")) 170 | }) 171 | }) 172 | 173 | Context("when docker fails to pull the image", func() { 174 | var pathToBadDocker, path string 175 | 176 | BeforeEach(func() { 177 | var err error 178 | pathToBadDocker, err = gexec.Build("github.com/ryanmoran/piper/fakes/docker", "-tags", "fail_pull") 179 | Expect(err).NotTo(HaveOccurred()) 180 | 181 | path = os.Getenv("PATH") 182 | os.Setenv("PATH", fmt.Sprintf("%s:%s", filepath.Dir(pathToBadDocker), os.Getenv("PATH"))) 183 | }) 184 | 185 | AfterEach(func() { 186 | os.Setenv("PATH", path) 187 | }) 188 | 189 | It("prints an error and exits 1", func() { 190 | command := exec.Command(pathToPiper, 191 | "-c", "fixtures/task.yml", 192 | "-i", "input-1=/tmp/local-1", 193 | "-o", "output-1=/tmp/local-2") 194 | session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) 195 | Expect(err).NotTo(HaveOccurred()) 196 | 197 | Eventually(session).Should(gexec.Exit(1)) 198 | Expect(session.Err.Contents()).To(ContainSubstring("failed to pull")) 199 | }) 200 | }) 201 | 202 | Context("when docker fails to run the command", func() { 203 | var pathToBadDocker, path string 204 | 205 | BeforeEach(func() { 206 | var err error 207 | pathToBadDocker, err = gexec.Build("github.com/ryanmoran/piper/fakes/docker", "-tags", "fail_run") 208 | Expect(err).NotTo(HaveOccurred()) 209 | 210 | path = os.Getenv("PATH") 211 | os.Setenv("PATH", fmt.Sprintf("%s:%s", filepath.Dir(pathToBadDocker), os.Getenv("PATH"))) 212 | }) 213 | 214 | AfterEach(func() { 215 | os.Setenv("PATH", path) 216 | }) 217 | 218 | It("prints an error and exits 1", func() { 219 | command := exec.Command(pathToPiper, 220 | "-c", "fixtures/task.yml", 221 | "-i", "input-1=/tmp/local-1", 222 | "-o", "output-1=/tmp/local-2") 223 | session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) 224 | Expect(err).NotTo(HaveOccurred()) 225 | 226 | Eventually(session).Should(gexec.Exit(1)) 227 | Expect(session.Err.Contents()).To(ContainSubstring("failed to run")) 228 | }) 229 | }) 230 | }) 231 | }) 232 | --------------------------------------------------------------------------------