From 94f88bac8400efa5b46710ae83c312deabe87eb5 Mon Sep 17 00:00:00 2001 From: Albert Salim Date: Wed, 3 Oct 2018 20:58:02 +0800 Subject: [PATCH] feat: Add templating support for docker `build-flags` I enhanced `BuildFlagTemplates` field in Docker image config to accept templates. Build flags are tested by checking for images created with labels applied through templates. See #813 --- internal/pipe/docker/docker.go | 55 +++++++++--- internal/pipe/docker/docker_test.go | 134 +++++++++++++++++++++++----- pkg/config/config.go | 20 ++--- 3 files changed, 165 insertions(+), 44 deletions(-) diff --git a/internal/pipe/docker/docker.go b/internal/pipe/docker/docker.go index 178aa0d46..3bbb5e25f 100644 --- a/internal/pipe/docker/docker.go +++ b/internal/pipe/docker/docker.go @@ -105,18 +105,12 @@ func process(ctx *context.Context, docker config.Docker, artifact artifact.Artif return errors.Wrap(err, "failed to create temporaty dir") } log.Debug("tempdir: " + tmp) - // nolint:prealloc - var images []string - for _, tagTemplate := range docker.TagTemplates { - // TODO: add overrides support to config - tag, err := tmpl.New(ctx). - WithArtifact(artifact, map[string]string{}). - Apply(tagTemplate) - if err != nil { - return errors.Wrapf(err, "failed to execute tag template '%s'", tagTemplate) - } - images = append(images, fmt.Sprintf("%s:%s", docker.Image, tag)) + + images, err := processTagTemplates(ctx, docker, artifact) + if err != nil { + return err } + if err := os.Link(docker.Dockerfile, filepath.Join(tmp, "Dockerfile")); err != nil { return errors.Wrap(err, "failed to link dockerfile") } @@ -128,7 +122,13 @@ func process(ctx *context.Context, docker config.Docker, artifact artifact.Artif if err := os.Link(artifact.Path, filepath.Join(tmp, filepath.Base(artifact.Path))); err != nil { return errors.Wrap(err, "failed to link binary") } - if err := dockerBuild(ctx, tmp, images[0], docker.BuildFlags); err != nil { + + buildFlags, err := processBuildFlagTemplates(ctx, docker, artifact) + if err != nil { + return err + } + + if err := dockerBuild(ctx, tmp, images[0], buildFlags); err != nil { return err } for _, img := range images[1:] { @@ -139,6 +139,37 @@ func process(ctx *context.Context, docker config.Docker, artifact artifact.Artif return publish(ctx, docker, images) } +func processTagTemplates(ctx *context.Context, docker config.Docker, artifact artifact.Artifact) ([]string, error) { + // nolint:prealloc + var images []string + for _, tagTemplate := range docker.TagTemplates { + // TODO: add overrides support to config + tag, err := tmpl.New(ctx). + WithArtifact(artifact, map[string]string{}). + Apply(tagTemplate) + if err != nil { + return nil, errors.Wrapf(err, "failed to execute tag template '%s'", tagTemplate) + } + images = append(images, fmt.Sprintf("%s:%s", docker.Image, tag)) + } + return images, nil +} + +func processBuildFlagTemplates(ctx *context.Context, docker config.Docker, artifact artifact.Artifact) ([]string, error) { + // nolint:prealloc + var buildFlags []string + for _, buildFlagTemplate := range docker.BuildFlagTemplates { + buildFlag, err := tmpl.New(ctx). + WithArtifact(artifact, map[string]string{}). + Apply(buildFlagTemplate) + if err != nil { + return nil, errors.Wrapf(err, "failed to process build flag template '%s'", buildFlagTemplate) + } + buildFlags = append(buildFlags, buildFlag) + } + return buildFlags, nil +} + // walks the src, recreating dirs and hard-linking files func link(src, dest string) error { return filepath.Walk(src, func(path string, info os.FileInfo, err error) error { diff --git a/internal/pipe/docker/docker_test.go b/internal/pipe/docker/docker_test.go index 630035fcb..19c13799f 100644 --- a/internal/pipe/docker/docker_test.go +++ b/internal/pipe/docker/docker_test.go @@ -2,10 +2,12 @@ package docker import ( "flag" + "fmt" "io/ioutil" "os" "os/exec" "path/filepath" + "regexp" "syscall" "testing" @@ -60,12 +62,29 @@ func TestRunPipe(t *testing.T) { var shouldNotErr = func(t *testing.T, err error) { require.NoError(t, err) } + type imageLabelFinder func(*testing.T, int, string) + var shouldFindImagesWithLabels = func(filters ...string) func(*testing.T, int, string) { + return func(t *testing.T, numTags int, image string) { + for _, filter := range filters { + output, err := exec.Command("docker", "images", "--filter", filter).CombinedOutput() + require.NoError(t, err) + fmt.Println(string(output)) + + matcher := regexp.MustCompile(image) + matches := matcher.FindAllStringIndex(string(output), -1) + require.Equal(t, numTags, len(matches)) + } + } + + } + var noLabels = func(t *testing.T, numTags int, image string) {} var table = map[string]struct { - dockers []config.Docker - publish bool - expect []string - assertError errChecker + dockers []config.Docker + publish bool + expect []string + assertImageLabels imageLabelFinder + assertError errChecker }{ "valid": { publish: true, @@ -80,10 +99,17 @@ func TestRunPipe(t *testing.T) { "{{.Tag}}-{{.Env.FOO}}", "v{{.Major}}", "v{{.Major}}.{{.Minor}}", - "commint-{{.Commit}}", + "commit-{{.Commit}}", "le-{{.Os}}", "latest", }, + BuildFlagTemplates: []string{ + "--label=org.label-schema.schema-version=1.0", + "--label=org.label-schema.version={{.Version}}", + "--label=org.label-schema.vcs-ref={{.Commit}}", + "--label=org.label-schema.name={{.ProjectName}}", + "--build-arg=FRED={{.Tag}}", + }, Files: []string{ "testdata/extra_file.txt", }, @@ -93,9 +119,15 @@ func TestRunPipe(t *testing.T) { registry + "goreleaser/test_run_pipe:v1.0.0-123", registry + "goreleaser/test_run_pipe:v1", registry + "goreleaser/test_run_pipe:v1.0", + registry + "goreleaser/test_run_pipe:commit-a1b2c3d4", registry + "goreleaser/test_run_pipe:le-linux", registry + "goreleaser/test_run_pipe:latest", }, + assertImageLabels: shouldFindImagesWithLabels( + "label=org.label-schema.schema-version=1.0", + "label=org.label-schema.version=1.0.0", + "label=org.label-schema.vcs-ref=a1b2c3d4", + "label=org.label-schema.name=mybin"), assertError: shouldNotErr, }, "multiple images with same extra file": { @@ -132,7 +164,8 @@ func TestRunPipe(t *testing.T) { registry + "goreleaser/multiplefiles1:latest", registry + "goreleaser/multiplefiles2:latest", }, - assertError: shouldNotErr, + assertImageLabels: noLabels, + assertError: shouldNotErr, }, "multiple images with same dockerfile": { publish: true, @@ -154,6 +187,7 @@ func TestRunPipe(t *testing.T) { TagTemplates: []string{"latest"}, }, }, + assertImageLabels: noLabels, expect: []string{ registry + "goreleaser/test_run_pipe:latest", registry + "goreleaser/test_run_pipe2:latest", @@ -187,7 +221,8 @@ func TestRunPipe(t *testing.T) { registry + "goreleaser/test_run_pipe:v1.0", registry + "goreleaser/test_run_pipe:latest", }, - assertError: shouldNotErr, + assertImageLabels: noLabels, + assertError: shouldNotErr, }, "valid_no_latest": { publish: true, @@ -209,7 +244,8 @@ func TestRunPipe(t *testing.T) { expect: []string{ registry + "goreleaser/test_run_pipe:1.0.0", }, - assertError: shouldNotErr, + assertImageLabels: noLabels, + assertError: shouldNotErr, }, "valid_dont_publish": { publish: false, @@ -233,7 +269,8 @@ func TestRunPipe(t *testing.T) { registry + "goreleaser/test_run_pipe:v1.0.0-123", registry + "goreleaser/test_run_pipe:latest", }, - assertError: shouldNotErr, + assertImageLabels: noLabels, + assertError: shouldNotErr, }, "valid build args": { publish: false, @@ -247,7 +284,7 @@ func TestRunPipe(t *testing.T) { TagTemplates: []string{ "latest", }, - BuildFlags: []string{ + BuildFlagTemplates: []string{ "--label=foo=bar", }, }, @@ -255,7 +292,8 @@ func TestRunPipe(t *testing.T) { expect: []string{ registry + "goreleaser/test_build_args:latest", }, - assertError: shouldNotErr, + assertImageLabels: noLabels, + assertError: shouldNotErr, }, "bad build args": { publish: false, @@ -269,12 +307,13 @@ func TestRunPipe(t *testing.T) { TagTemplates: []string{ "latest", }, - BuildFlags: []string{ + BuildFlagTemplates: []string{ "--bad-flag", }, }, }, - assertError: shouldErr("unknown flag: --bad-flag"), + assertImageLabels: noLabels, + assertError: shouldErr("unknown flag: --bad-flag"), }, "bad_dockerfile": { publish: true, @@ -290,9 +329,10 @@ func TestRunPipe(t *testing.T) { }, }, }, - assertError: shouldErr("pull access denied for nope, repository does not exist"), + assertImageLabels: noLabels, + assertError: shouldErr("pull access denied for nope, repository does not exist"), }, - "template_error": { + "tag_template_error": { publish: true, dockers: []config.Docker{ { @@ -306,9 +346,30 @@ func TestRunPipe(t *testing.T) { }, }, }, - assertError: shouldErr(`template: tmpl:1: unexpected "}" in operand`), + assertImageLabels: noLabels, + assertError: shouldErr(`template: tmpl:1: unexpected "}" in operand`), }, - "missing_env_on_template": { + "build_flag_template_error": { + publish: true, + dockers: []config.Docker{ + { + Image: registry + "goreleaser/test_run_pipe", + Goos: "linux", + Goarch: "amd64", + Dockerfile: "testdata/Dockerfile", + Binary: "mybin", + TagTemplates: []string{ + "latest", + }, + BuildFlagTemplates: []string{ + "--label=tag={{.Tag}", + }, + }, + }, + assertImageLabels: noLabels, + assertError: shouldErr(`template: tmpl:1: unexpected "}" in operand`), + }, + "missing_env_on_tag_template": { publish: true, dockers: []config.Docker{ { @@ -322,7 +383,28 @@ func TestRunPipe(t *testing.T) { }, }, }, - assertError: shouldErr(`template: tmpl:1:6: executing "tmpl" at <.Env.NOPE>: map has no entry for key "NOPE"`), + assertImageLabels: noLabels, + assertError: shouldErr(`template: tmpl:1:6: executing "tmpl" at <.Env.NOPE>: map has no entry for key "NOPE"`), + }, + "missing_env_on_build_flag_template": { + publish: true, + dockers: []config.Docker{ + { + Image: registry + "goreleaser/test_run_pipe", + Goos: "linux", + Goarch: "amd64", + Dockerfile: "testdata/Dockerfile", + Binary: "mybin", + TagTemplates: []string{ + "latest", + }, + BuildFlagTemplates: []string{ + "--label=nope={{.Env.NOPE}}", + }, + }, + }, + assertImageLabels: noLabels, + assertError: shouldErr(`template: tmpl:1:19: executing "tmpl" at <.Env.NOPE>: map has no entry for key "NOPE"`), }, "no_permissions": { publish: true, @@ -343,7 +425,8 @@ func TestRunPipe(t *testing.T) { "docker.io/nope:latest", "docker.io/nope:v1.0.0", }, - assertError: shouldErr(`requested access to the resource is denied`), + assertImageLabels: noLabels, + assertError: shouldErr(`requested access to the resource is denied`), }, "dockerfile_doesnt_exist": { publish: true, @@ -359,7 +442,8 @@ func TestRunPipe(t *testing.T) { }, }, }, - assertError: shouldErr(`failed to link dockerfile`), + assertImageLabels: noLabels, + assertError: shouldErr(`failed to link dockerfile`), }, "extra_file_doesnt_exist": { publish: true, @@ -378,7 +462,8 @@ func TestRunPipe(t *testing.T) { }, }, }, - assertError: shouldErr(`failed to link extra file 'testdata/nope.txt'`), + assertImageLabels: noLabels, + assertError: shouldErr(`failed to link extra file 'testdata/nope.txt'`), }, "no_matching_binaries": { publish: true, @@ -391,7 +476,8 @@ func TestRunPipe(t *testing.T) { Dockerfile: "testdata/Dockerfile", }, }, - assertError: shouldErr(`0 binaries match docker definition: mybinnnn: darwin_amd64_`), + assertImageLabels: noLabels, + assertError: shouldErr(`0 binaries match docker definition: mybinnnn: darwin_amd64_`), }, } @@ -422,6 +508,7 @@ func TestRunPipe(t *testing.T) { ctx.Version = "1.0.0" ctx.Git = context.GitInfo{ CurrentTag: "v1.0.0", + Commit: "a1b2c3d4", } for _, os := range []string{"linux", "darwin"} { for _, arch := range []string{"amd64", "386"} { @@ -444,6 +531,9 @@ func TestRunPipe(t *testing.T) { } docker.assertError(tt, Pipe{}.Run(ctx)) + for _, d := range docker.dockers { + docker.assertImageLabels(tt, len(d.TagTemplates), d.Image) + } // this might should not fail as the image should have been created when // the step ran diff --git a/pkg/config/config.go b/pkg/config/config.go index 7766d640a..13ecb926f 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -238,16 +238,16 @@ type Checksum struct { // Docker image config type Docker struct { - Binary string `yaml:",omitempty"` - Goos string `yaml:",omitempty"` - Goarch string `yaml:",omitempty"` - Goarm string `yaml:",omitempty"` - Image string `yaml:",omitempty"` - Dockerfile string `yaml:",omitempty"` - SkipPush bool `yaml:"skip_push,omitempty"` - TagTemplates []string `yaml:"tag_templates,omitempty"` - Files []string `yaml:"extra_files,omitempty"` - BuildFlags []string `yaml:"build_flags,omitempty"` + Binary string `yaml:",omitempty"` + Goos string `yaml:",omitempty"` + Goarch string `yaml:",omitempty"` + Goarm string `yaml:",omitempty"` + Image string `yaml:",omitempty"` + Dockerfile string `yaml:",omitempty"` + SkipPush bool `yaml:"skip_push,omitempty"` + TagTemplates []string `yaml:"tag_templates,omitempty"` + Files []string `yaml:"extra_files,omitempty"` + BuildFlagTemplates []string `yaml:"build_flag_templates,omitempty"` } // Filters config