github.com/droot/goreleaser@v0.66.2-0.20180420030140-c2db5fb17157/pipeline/docker/docker.go (about) 1 // Package docker provides a Pipe that creates and pushes a Docker image 2 package docker 3 4 import ( 5 "bytes" 6 "fmt" 7 "os" 8 "os/exec" 9 "path/filepath" 10 "strings" 11 "text/template" 12 13 "github.com/apex/log" 14 "github.com/masterminds/semver" 15 "github.com/pkg/errors" 16 "golang.org/x/sync/errgroup" 17 18 "github.com/goreleaser/goreleaser/config" 19 "github.com/goreleaser/goreleaser/context" 20 "github.com/goreleaser/goreleaser/internal/artifact" 21 "github.com/goreleaser/goreleaser/internal/deprecate" 22 "github.com/goreleaser/goreleaser/pipeline" 23 ) 24 25 // ErrNoDocker is shown when docker cannot be found in $PATH 26 var ErrNoDocker = errors.New("docker not present in $PATH") 27 28 // Pipe for docker 29 type Pipe struct{} 30 31 func (Pipe) String() string { 32 return "creating Docker images" 33 } 34 35 // Default sets the pipe defaults 36 func (Pipe) Default(ctx *context.Context) error { 37 for i := range ctx.Config.Dockers { 38 var docker = &ctx.Config.Dockers[i] 39 if docker.OldTagTemplate != "" { 40 deprecate.Notice("docker.tag_template") 41 docker.TagTemplates = append(docker.TagTemplates, docker.OldTagTemplate) 42 } 43 if len(docker.TagTemplates) == 0 { 44 docker.TagTemplates = append(docker.TagTemplates, "{{ .Version }}") 45 } 46 if docker.Goos == "" { 47 docker.Goos = "linux" 48 } 49 if docker.Goarch == "" { 50 docker.Goarch = "amd64" 51 } 52 if docker.Latest { 53 deprecate.Notice("docker.latest") 54 docker.TagTemplates = append(docker.TagTemplates, "latest") 55 } 56 } 57 // only set defaults if there is exacly 1 docker setup in the config file. 58 if len(ctx.Config.Dockers) != 1 { 59 return nil 60 } 61 if ctx.Config.Dockers[0].Binary == "" { 62 ctx.Config.Dockers[0].Binary = ctx.Config.Builds[0].Binary 63 } 64 if ctx.Config.Dockers[0].Dockerfile == "" { 65 ctx.Config.Dockers[0].Dockerfile = "Dockerfile" 66 } 67 return nil 68 } 69 70 // Run the pipe 71 func (Pipe) Run(ctx *context.Context) error { 72 if len(ctx.Config.Dockers) == 0 || ctx.Config.Dockers[0].Image == "" { 73 return pipeline.Skip("docker section is not configured") 74 } 75 _, err := exec.LookPath("docker") 76 if err != nil { 77 return ErrNoDocker 78 } 79 return doRun(ctx) 80 } 81 82 func doRun(ctx *context.Context) error { 83 var g errgroup.Group 84 sem := make(chan bool, ctx.Parallelism) 85 for _, docker := range ctx.Config.Dockers { 86 docker := docker 87 sem <- true 88 g.Go(func() error { 89 defer func() { 90 <-sem 91 }() 92 log.WithField("docker", docker).Debug("looking for binaries matching") 93 var binaries = ctx.Artifacts.Filter( 94 artifact.And( 95 artifact.ByGoos(docker.Goos), 96 artifact.ByGoarch(docker.Goarch), 97 artifact.ByGoarm(docker.Goarm), 98 artifact.ByType(artifact.Binary), 99 func(a artifact.Artifact) bool { 100 return a.Extra["Binary"] == docker.Binary 101 }, 102 ), 103 ).List() 104 if len(binaries) == 0 { 105 log.Warnf("no binaries found for %s", docker.Binary) 106 } 107 for _, binary := range binaries { 108 if err := process(ctx, docker, binary); err != nil { 109 return err 110 } 111 } 112 return nil 113 }) 114 } 115 return g.Wait() 116 } 117 118 func tagName(ctx *context.Context, tagTemplate string) (string, error) { 119 var out bytes.Buffer 120 t, err := template.New("tag").Option("missingkey=error").Parse(tagTemplate) 121 if err != nil { 122 return "", err 123 } 124 sv, err := semver.NewVersion(ctx.Git.CurrentTag) 125 if err != nil { 126 return "", err 127 } 128 data := struct { 129 Version, Tag string 130 Major, Minor, Patch int64 131 Env map[string]string 132 }{ 133 Version: ctx.Version, 134 Tag: ctx.Git.CurrentTag, 135 Env: ctx.Env, 136 Major: sv.Major(), 137 Minor: sv.Minor(), 138 Patch: sv.Patch(), 139 } 140 err = t.Execute(&out, data) 141 return out.String(), err 142 } 143 144 func process(ctx *context.Context, docker config.Docker, artifact artifact.Artifact) error { 145 var root = filepath.Dir(artifact.Path) 146 var dockerfile = filepath.Join(root, filepath.Base(docker.Dockerfile)) 147 var images []string 148 for _, tagTemplate := range docker.TagTemplates { 149 tag, err := tagName(ctx, tagTemplate) 150 if err != nil { 151 return errors.Wrapf(err, "failed to execute tag template '%s'", tagTemplate) 152 } 153 images = append(images, fmt.Sprintf("%s:%s", docker.Image, tag)) 154 } 155 if err := os.Link(docker.Dockerfile, dockerfile); err != nil { 156 return errors.Wrap(err, "failed to link dockerfile") 157 } 158 for _, file := range docker.Files { 159 if err := link(file, filepath.Join(root, filepath.Base(file))); err != nil { 160 return errors.Wrapf(err, "failed to link extra file '%s'", file) 161 } 162 } 163 if err := dockerBuild(ctx, root, dockerfile, images[0]); err != nil { 164 return err 165 } 166 for _, img := range images[1:] { 167 if err := dockerTag(ctx, images[0], img); err != nil { 168 return err 169 } 170 } 171 return publish(ctx, docker, images) 172 } 173 174 // walks the src, recreating dirs and hard-linking files 175 func link(src, dest string) error { 176 return filepath.Walk(src, func(path string, info os.FileInfo, err error) error { 177 if err != nil { 178 return err 179 } 180 // We have the following: 181 // - src = "a/b" 182 // - dest = "dist/linuxamd64/b" 183 // - path = "a/b/c.txt" 184 // So we join "a/b" with "c.txt" and use it as the destination. 185 var dst = filepath.Join(dest, strings.Replace(path, src, "", 1)) 186 log.WithFields(log.Fields{ 187 "src": path, 188 "dst": dst, 189 }).Info("extra file") 190 if info.IsDir() { 191 return os.MkdirAll(dst, info.Mode()) 192 } 193 return os.Link(path, dst) 194 }) 195 } 196 197 func publish(ctx *context.Context, docker config.Docker, images []string) error { 198 if ctx.SkipPublish { 199 // TODO: this should be better handled 200 log.Warn(pipeline.ErrSkipPublishEnabled.Error()) 201 return nil 202 } 203 if docker.SkipPush { 204 // TODO: this should also be better handled 205 log.Warn(pipeline.Skip("skip_push is set").Error()) 206 return nil 207 } 208 for _, image := range images { 209 if err := dockerPush(ctx, docker, image); err != nil { 210 return err 211 } 212 } 213 return nil 214 } 215 216 func dockerBuild(ctx *context.Context, root, dockerfile, image string) error { 217 log.WithField("image", image).Info("building docker image") 218 /* #nosec */ 219 var cmd = exec.CommandContext(ctx, "docker", "build", "-f", dockerfile, "-t", image, root) 220 log.WithField("cmd", cmd.Args).Debug("running") 221 out, err := cmd.CombinedOutput() 222 if err != nil { 223 return errors.Wrapf(err, "failed to build docker image: \n%s", string(out)) 224 } 225 log.Debugf("docker build output: \n%s", string(out)) 226 return nil 227 } 228 229 func dockerTag(ctx *context.Context, image, tag string) error { 230 log.WithField("image", image).WithField("tag", tag).Info("tagging docker image") 231 /* #nosec */ 232 var cmd = exec.CommandContext(ctx, "docker", "tag", image, tag) 233 log.WithField("cmd", cmd.Args).Debug("running") 234 out, err := cmd.CombinedOutput() 235 if err != nil { 236 return errors.Wrapf(err, "failed to tag docker image: \n%s", string(out)) 237 } 238 log.Debugf("docker tag output: \n%s", string(out)) 239 return nil 240 } 241 242 func dockerPush(ctx *context.Context, docker config.Docker, image string) error { 243 log.WithField("image", image).Info("pushing docker image") 244 /* #nosec */ 245 var cmd = exec.CommandContext(ctx, "docker", "push", image) 246 log.WithField("cmd", cmd.Args).Debug("running") 247 out, err := cmd.CombinedOutput() 248 if err != nil { 249 return errors.Wrapf(err, "failed to push docker image: \n%s", string(out)) 250 } 251 log.Debugf("docker push output: \n%s", string(out)) 252 ctx.Artifacts.Add(artifact.Artifact{ 253 Type: artifact.DockerImage, 254 Name: image, 255 Path: image, 256 Goarch: docker.Goarch, 257 Goos: docker.Goos, 258 Goarm: docker.Goarm, 259 }) 260 return nil 261 }