Skip to content
This repository was archived by the owner on Jul 18, 2025. It is now read-only.

Commit 09d931a

Browse files
Ulysses Souzaulyssessouza
authored andcommitted
Implement multi tag for build
Signed-off-by: Ulysses Souza <[email protected]>
1 parent 9169a3c commit 09d931a

File tree

4 files changed

+109
-25
lines changed

4 files changed

+109
-25
lines changed

e2e/build_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,43 @@ func TestBuild(t *testing.T) {
5858
})
5959
}
6060

61+
func TestBuildMultiTag(t *testing.T) {
62+
runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) {
63+
cmd := info.configuredCmd
64+
tmp := fs.NewDir(t, "TestBuild")
65+
testDir := path.Join("testdata", "build")
66+
iidfile := tmp.Join("iidfile")
67+
tags := []string{"1.0.0", "latest"}
68+
cmd.Command = dockerCli.Command("app", "build", "--tag", "single:"+tags[0], "--tag", "single:"+tags[1], "--iidfile", iidfile, "-f", path.Join(testDir, "single.dockerapp"), testDir)
69+
icmd.RunCmd(cmd).Assert(t, icmd.Success)
70+
71+
cfg := getDockerConfigDir(t, cmd)
72+
73+
for _, tag := range tags {
74+
f := path.Join(cfg, "app", "bundles", "docker.io", "library", "single", "_tags", tag, relocated.BundleFilename)
75+
bndl, err := relocated.BundleFromFile(f)
76+
assert.NilError(t, err)
77+
built := []string{bndl.InvocationImages[0].Digest, bndl.Images["web"].Digest, bndl.Images["worker"].Digest}
78+
for _, ref := range built {
79+
cmd.Command = dockerCli.Command("inspect", ref)
80+
icmd.RunCmd(cmd).Assert(t, icmd.Success)
81+
}
82+
for _, img := range bndl.Images {
83+
// Check all image not being built locally get a fixed reference
84+
assert.Assert(t, img.Image == "" || strings.Contains(img.Image, "@sha256:"))
85+
}
86+
_, err = os.Stat(iidfile)
87+
assert.NilError(t, err)
88+
bytes, err := ioutil.ReadFile(iidfile)
89+
assert.NilError(t, err)
90+
iid := string(bytes)
91+
actualID, err := store.FromBundle(bndl)
92+
assert.NilError(t, err)
93+
assert.Equal(t, iid, fmt.Sprintf("sha256:%s", actualID.String()))
94+
}
95+
})
96+
}
97+
6198
func TestQuietBuild(t *testing.T) {
6299
runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) {
63100
cmd := info.configuredCmd

internal/commands/build/build.go

Lines changed: 70 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import (
55
"context"
66
"encoding/json"
77
"fmt"
8+
"io"
89
"io/ioutil"
910
"os"
1011
"path/filepath"
1112
"strconv"
1213
"strings"
14+
"sync"
1315

1416
"github.com/deislabs/cnab-go/bundle"
1517
cnab "github.com/deislabs/cnab-go/driver"
@@ -24,6 +26,7 @@ import (
2426
"github.com/docker/cli/cli/command"
2527
compose "github.com/docker/cli/cli/compose/types"
2628
"github.com/docker/cli/cli/streams"
29+
cliOpts "github.com/docker/cli/opts"
2730
"github.com/docker/cnab-to-oci/remotes"
2831
"github.com/docker/distribution/reference"
2932
"github.com/moby/buildkit/client"
@@ -39,19 +42,26 @@ type buildOptions struct {
3942
noCache bool
4043
progress string
4144
pull bool
42-
tag string
45+
tags cliOpts.ListOpts
4346
folder string
4447
imageIDFile string
45-
args []string
48+
args cliOpts.ListOpts
4649
quiet bool
4750
noResolveImage bool
4851
}
4952

5053
const buildExample = `- $ docker app build .
51-
- $ docker app build --file myapp.dockerapp --tag myrepo/myapp:1.0.0 .`
54+
- $ docker app build --file myapp.dockerapp --tag myrepo/myapp:1.0.0 --tag myrepo/myapp:latest .`
55+
56+
func newBuildOptions() buildOptions {
57+
return buildOptions{
58+
tags: cliOpts.NewListOpts(validateTag),
59+
args: cliOpts.NewListOpts(nil),
60+
}
61+
}
5262

5363
func Cmd(dockerCli command.Cli) *cobra.Command {
54-
var opts buildOptions
64+
opts := newBuildOptions()
5565
cmd := &cobra.Command{
5666
Use: "build [OPTIONS] BUILD_PATH",
5767
Short: "Build an App image from an App definition (.dockerapp)",
@@ -66,10 +76,10 @@ func Cmd(dockerCli command.Cli) *cobra.Command {
6676
flags.BoolVar(&opts.noCache, "no-cache", false, "Do not use cache when building the App image")
6777
flags.StringVar(&opts.progress, "progress", "auto", "Set type of progress output (auto, plain, tty). Use plain to show container output")
6878
flags.BoolVar(&opts.noResolveImage, "no-resolve-image", false, "Do not query the registry to resolve image digest")
69-
flags.StringVarP(&opts.tag, "tag", "t", "", "App image tag, optionally in the 'repo:tag' format")
79+
flags.VarP(&opts.tags, "tag", "t", "Name and optionally a tag in the 'name:tag' format")
7080
flags.StringVarP(&opts.folder, "file", "f", "", "App definition as a .dockerapp directory")
7181
flags.BoolVar(&opts.pull, "pull", false, "Always attempt to pull a newer version of the App image")
72-
flags.StringArrayVar(&opts.args, "build-arg", []string{}, "Set build-time variables")
82+
flags.Var(&opts.args, "build-arg", "Set build-time variables")
7383
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress the build output and print App image ID on success")
7484
flags.StringVar(&opts.imageIDFile, "iidfile", "", "Write the App image ID to the file")
7585

@@ -128,37 +138,65 @@ func runBuild(dockerCli command.Cli, contextPath string, opt buildOptions) error
128138
}
129139
defer app.Cleanup()
130140

131-
bundle, err := buildImageUsingBuildx(app, contextPath, opt, dockerCli)
141+
bndl, err := buildImageUsingBuildx(app, contextPath, opt, dockerCli)
132142
if err != nil {
133143
return err
134144
}
135145

136-
var ref reference.Reference
137-
ref, err = packager.GetNamedTagged(opt.tag)
146+
out, err := getOutputFile(dockerCli.Out(), opt.quiet)
138147
if err != nil {
139148
return err
140149
}
141150

142-
id, err := packager.PersistInBundleStore(ref, bundle)
151+
id, err := persistTags(bndl, opt.tags, opt.imageIDFile, out, dockerCli.Err())
143152
if err != nil {
144153
return err
145154
}
146155

147-
if opt.imageIDFile != "" {
148-
if err = ioutil.WriteFile(opt.imageIDFile, []byte(id.Digest().String()), 0644); err != nil {
149-
fmt.Fprintf(dockerCli.Err(), "Failed to write App image ID in %s: %s", opt.imageIDFile, err)
150-
}
156+
if opt.quiet {
157+
_, err = fmt.Fprintln(dockerCli.Out(), id.Digest().String())
151158
}
159+
return err
160+
}
152161

153-
if opt.quiet {
154-
fmt.Fprintln(dockerCli.Out(), id.Digest().String())
155-
return err
162+
func persistTags(bndl *bundle.Bundle, tags cliOpts.ListOpts, iidFile string, outWriter io.Writer, errWriter io.Writer) (reference.Digested, error) {
163+
var (
164+
id reference.Digested
165+
onceWriteIIDFile sync.Once
166+
)
167+
if tags.Len() == 0 {
168+
return persistInBundleStore(&onceWriteIIDFile, outWriter, errWriter, bndl, nil, iidFile)
169+
}
170+
for _, tag := range tags.GetAll() {
171+
ref, err := packager.GetNamedTagged(tag)
172+
if err != nil {
173+
return nil, err
174+
}
175+
id, err = persistInBundleStore(&onceWriteIIDFile, outWriter, errWriter, bndl, ref, iidFile)
176+
if err != nil {
177+
return nil, err
178+
}
179+
if tag != "" {
180+
fmt.Fprintf(outWriter, "Successfully tagged app image %s\n", ref.String())
181+
}
156182
}
157-
fmt.Fprintf(dockerCli.Out(), "Successfully built %s\n", id.String())
158-
if ref != nil {
159-
fmt.Fprintf(dockerCli.Out(), "Successfully tagged %s\n", ref.String())
183+
return id, nil
184+
}
185+
186+
func persistInBundleStore(once *sync.Once, outWriter io.Writer, errWriter io.Writer, b *bundle.Bundle, ref reference.Reference, iidFileName string) (reference.Digested, error) {
187+
id, err := packager.PersistInBundleStore(ref, b)
188+
if err != nil {
189+
return nil, err
160190
}
161-
return err
191+
once.Do(func() {
192+
fmt.Fprintf(outWriter, "Successfully built app image %s\n", id.String())
193+
if iidFileName != "" {
194+
if err := ioutil.WriteFile(iidFileName, []byte(id.Digest().String()), 0644); err != nil {
195+
fmt.Fprintf(errWriter, "Failed to write App image ID in %s: %s", iidFileName, err)
196+
}
197+
}
198+
})
199+
return id, nil
162200
}
163201

164202
func buildImageUsingBuildx(app *types.App, contextPath string, opt buildOptions, dockerCli command.Cli) (*bundle.Bundle, error) {
@@ -350,9 +388,9 @@ func debugSolveResponses(resp map[string]*client.SolveResponse) {
350388
}
351389
}
352390

353-
func checkBuildArgsUniqueness(args []string) error {
391+
func checkBuildArgsUniqueness(args cliOpts.ListOpts) error {
354392
set := make(map[string]bool)
355-
for _, value := range args {
393+
for _, value := range args.GetAllOrEmpty() {
356394
key := strings.Split(value, "=")[0]
357395
if _, ok := set[key]; ok {
358396
return fmt.Errorf("'--build-arg %s' is defined twice", key)
@@ -361,3 +399,12 @@ func checkBuildArgsUniqueness(args []string) error {
361399
}
362400
return nil
363401
}
402+
403+
// validateTag checks if the given image name can be resolved.
404+
func validateTag(rawRepo string) (string, error) {
405+
_, err := reference.ParseNormalizedNamed(rawRepo)
406+
if err != nil {
407+
return "", err
408+
}
409+
return rawRepo, nil
410+
}

internal/commands/build/compose.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ func parseCompose(app *types.App, contextPath string, options buildOptions) (map
2121
return nil, nil, err
2222
}
2323

24-
buildArgs := buildArgsToMap(options.args)
24+
buildArgs := buildArgsToMap(options.args.GetAllOrEmpty())
2525

2626
pulledServices := []compose.ServiceConfig{}
2727
opts := map[string]build.Options{}

internal/commands/build/compose_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ func Test_parseCompose(t *testing.T) {
5151
t.Run(tt.name, func(t *testing.T) {
5252
app, err := packager.Extract("testdata/" + tt.name)
5353
assert.NilError(t, err)
54-
got, _, err := parseCompose(app, "testdata", buildOptions{})
54+
got, _, err := parseCompose(app, "testdata", newBuildOptions())
5555
assert.NilError(t, err)
5656
_, ok := got["dontwant"]
5757
assert.Assert(t, !ok, "parseCompose() should have excluded 'dontwant' service")

0 commit comments

Comments
 (0)