github.com/szyn/goreleaser@v0.76.1-0.20180517112710-333da09a1297/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 string 130 Tag string 131 Commit string 132 Major int64 133 Minor int64 134 Patch int64 135 Env map[string]string 136 }{ 137 Version: ctx.Version, 138 Commit: ctx.Git.Commit, 139 Tag: ctx.Git.CurrentTag, 140 Env: ctx.Env, 141 Major: sv.Major(), 142 Minor: sv.Minor(), 143 Patch: sv.Patch(), 144 } 145 err = t.Execute(&out, data) 146 return out.String(), err 147 } 148 149 func process(ctx *context.Context, docker config.Docker, artifact artifact.Artifact) error { 150 var root = filepath.Dir(artifact.Path) 151 var dockerfile = filepath.Join(root, filepath.Base(docker.Dockerfile)) 152 var images []string 153 for _, tagTemplate := range docker.TagTemplates { 154 tag, err := tagName(ctx, tagTemplate) 155 if err != nil { 156 return errors.Wrapf(err, "failed to execute tag template '%s'", tagTemplate) 157 } 158 images = append(images, fmt.Sprintf("%s:%s", docker.Image, tag)) 159 } 160 if err := os.Link(docker.Dockerfile, dockerfile); err != nil { 161 return errors.Wrap(err, "failed to link dockerfile") 162 } 163 for _, file := range docker.Files { 164 if err := link(file, filepath.Join(root, filepath.Base(file))); err != nil { 165 return errors.Wrapf(err, "failed to link extra file '%s'", file) 166 } 167 } 168 if err := dockerBuild(ctx, root, dockerfile, images[0]); err != nil { 169 return err 170 } 171 for _, img := range images[1:] { 172 if err := dockerTag(ctx, images[0], img); err != nil { 173 return err 174 } 175 } 176 return publish(ctx, docker, images) 177 } 178 179 // walks the src, recreating dirs and hard-linking files 180 func link(src, dest string) error { 181 return filepath.Walk(src, func(path string, info os.FileInfo, err error) error { 182 if err != nil { 183 return err 184 } 185 // We have the following: 186 // - src = "a/b" 187 // - dest = "dist/linuxamd64/b" 188 // - path = "a/b/c.txt" 189 // So we join "a/b" with "c.txt" and use it as the destination. 190 var dst = filepath.Join(dest, strings.Replace(path, src, "", 1)) 191 log.WithFields(log.Fields{ 192 "src": path, 193 "dst": dst, 194 }).Info("extra file") 195 if info.IsDir() { 196 return os.MkdirAll(dst, info.Mode()) 197 } 198 return os.Link(path, dst) 199 }) 200 } 201 202 func publish(ctx *context.Context, docker config.Docker, images []string) error { 203 if ctx.SkipPublish { 204 // TODO: this should be better handled 205 log.Warn(pipeline.ErrSkipPublishEnabled.Error()) 206 return nil 207 } 208 if docker.SkipPush { 209 // TODO: this should also be better handled 210 log.Warn(pipeline.Skip("skip_push is set").Error()) 211 return nil 212 } 213 for _, image := range images { 214 if err := dockerPush(ctx, docker, image); err != nil { 215 return err 216 } 217 } 218 return nil 219 } 220 221 func dockerBuild(ctx *context.Context, root, dockerfile, image string) error { 222 log.WithField("image", image).Info("building docker image") 223 /* #nosec */ 224 var cmd = exec.CommandContext(ctx, "docker", "build", "-f", dockerfile, "-t", image, root) 225 log.WithField("cmd", cmd.Args).Debug("running") 226 out, err := cmd.CombinedOutput() 227 if err != nil { 228 return errors.Wrapf(err, "failed to build docker image: \n%s", string(out)) 229 } 230 log.Debugf("docker build output: \n%s", string(out)) 231 return nil 232 } 233 234 func dockerTag(ctx *context.Context, image, tag string) error { 235 log.WithField("image", image).WithField("tag", tag).Info("tagging docker image") 236 /* #nosec */ 237 var cmd = exec.CommandContext(ctx, "docker", "tag", image, tag) 238 log.WithField("cmd", cmd.Args).Debug("running") 239 out, err := cmd.CombinedOutput() 240 if err != nil { 241 return errors.Wrapf(err, "failed to tag docker image: \n%s", string(out)) 242 } 243 log.Debugf("docker tag output: \n%s", string(out)) 244 return nil 245 } 246 247 func dockerPush(ctx *context.Context, docker config.Docker, image string) error { 248 log.WithField("image", image).Info("pushing docker image") 249 /* #nosec */ 250 var cmd = exec.CommandContext(ctx, "docker", "push", image) 251 log.WithField("cmd", cmd.Args).Debug("running") 252 out, err := cmd.CombinedOutput() 253 if err != nil { 254 return errors.Wrapf(err, "failed to push docker image: \n%s", string(out)) 255 } 256 log.Debugf("docker push output: \n%s", string(out)) 257 ctx.Artifacts.Add(artifact.Artifact{ 258 Type: artifact.DockerImage, 259 Name: image, 260 Path: image, 261 Goarch: docker.Goarch, 262 Goos: docker.Goos, 263 Goarm: docker.Goarm, 264 }) 265 return nil 266 }