Skip to content
This repository has been archived by the owner on Jun 13, 2021. It is now read-only.

Commit

Permalink
Implement multi tag for build
Browse files Browse the repository at this point in the history
Signed-off-by: Ulysses Souza <[email protected]>
  • Loading branch information
ulyssessouza committed Dec 3, 2019
1 parent 9169a3c commit 09d931a
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 25 deletions.
37 changes: 37 additions & 0 deletions e2e/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,43 @@ func TestBuild(t *testing.T) {
})
}

func TestBuildMultiTag(t *testing.T) {
runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) {
cmd := info.configuredCmd
tmp := fs.NewDir(t, "TestBuild")
testDir := path.Join("testdata", "build")
iidfile := tmp.Join("iidfile")
tags := []string{"1.0.0", "latest"}
cmd.Command = dockerCli.Command("app", "build", "--tag", "single:"+tags[0], "--tag", "single:"+tags[1], "--iidfile", iidfile, "-f", path.Join(testDir, "single.dockerapp"), testDir)
icmd.RunCmd(cmd).Assert(t, icmd.Success)

cfg := getDockerConfigDir(t, cmd)

for _, tag := range tags {
f := path.Join(cfg, "app", "bundles", "docker.io", "library", "single", "_tags", tag, relocated.BundleFilename)
bndl, err := relocated.BundleFromFile(f)
assert.NilError(t, err)
built := []string{bndl.InvocationImages[0].Digest, bndl.Images["web"].Digest, bndl.Images["worker"].Digest}
for _, ref := range built {
cmd.Command = dockerCli.Command("inspect", ref)
icmd.RunCmd(cmd).Assert(t, icmd.Success)
}
for _, img := range bndl.Images {
// Check all image not being built locally get a fixed reference
assert.Assert(t, img.Image == "" || strings.Contains(img.Image, "@sha256:"))
}
_, err = os.Stat(iidfile)
assert.NilError(t, err)
bytes, err := ioutil.ReadFile(iidfile)
assert.NilError(t, err)
iid := string(bytes)
actualID, err := store.FromBundle(bndl)
assert.NilError(t, err)
assert.Equal(t, iid, fmt.Sprintf("sha256:%s", actualID.String()))
}
})
}

func TestQuietBuild(t *testing.T) {
runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) {
cmd := info.configuredCmd
Expand Down
93 changes: 70 additions & 23 deletions internal/commands/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"sync"

"github.com/deislabs/cnab-go/bundle"
cnab "github.com/deislabs/cnab-go/driver"
Expand All @@ -24,6 +26,7 @@ import (
"github.com/docker/cli/cli/command"
compose "github.com/docker/cli/cli/compose/types"
"github.com/docker/cli/cli/streams"
cliOpts "github.com/docker/cli/opts"
"github.com/docker/cnab-to-oci/remotes"
"github.com/docker/distribution/reference"
"github.com/moby/buildkit/client"
Expand All @@ -39,19 +42,26 @@ type buildOptions struct {
noCache bool
progress string
pull bool
tag string
tags cliOpts.ListOpts
folder string
imageIDFile string
args []string
args cliOpts.ListOpts
quiet bool
noResolveImage bool
}

const buildExample = `- $ docker app build .
- $ docker app build --file myapp.dockerapp --tag myrepo/myapp:1.0.0 .`
- $ docker app build --file myapp.dockerapp --tag myrepo/myapp:1.0.0 --tag myrepo/myapp:latest .`

func newBuildOptions() buildOptions {
return buildOptions{
tags: cliOpts.NewListOpts(validateTag),
args: cliOpts.NewListOpts(nil),
}
}

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

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

bundle, err := buildImageUsingBuildx(app, contextPath, opt, dockerCli)
bndl, err := buildImageUsingBuildx(app, contextPath, opt, dockerCli)
if err != nil {
return err
}

var ref reference.Reference
ref, err = packager.GetNamedTagged(opt.tag)
out, err := getOutputFile(dockerCli.Out(), opt.quiet)
if err != nil {
return err
}

id, err := packager.PersistInBundleStore(ref, bundle)
id, err := persistTags(bndl, opt.tags, opt.imageIDFile, out, dockerCli.Err())
if err != nil {
return err
}

if opt.imageIDFile != "" {
if err = ioutil.WriteFile(opt.imageIDFile, []byte(id.Digest().String()), 0644); err != nil {
fmt.Fprintf(dockerCli.Err(), "Failed to write App image ID in %s: %s", opt.imageIDFile, err)
}
if opt.quiet {
_, err = fmt.Fprintln(dockerCli.Out(), id.Digest().String())
}
return err
}

if opt.quiet {
fmt.Fprintln(dockerCli.Out(), id.Digest().String())
return err
func persistTags(bndl *bundle.Bundle, tags cliOpts.ListOpts, iidFile string, outWriter io.Writer, errWriter io.Writer) (reference.Digested, error) {
var (
id reference.Digested
onceWriteIIDFile sync.Once
)
if tags.Len() == 0 {
return persistInBundleStore(&onceWriteIIDFile, outWriter, errWriter, bndl, nil, iidFile)
}
for _, tag := range tags.GetAll() {
ref, err := packager.GetNamedTagged(tag)
if err != nil {
return nil, err
}
id, err = persistInBundleStore(&onceWriteIIDFile, outWriter, errWriter, bndl, ref, iidFile)
if err != nil {
return nil, err
}
if tag != "" {
fmt.Fprintf(outWriter, "Successfully tagged app image %s\n", ref.String())
}
}
fmt.Fprintf(dockerCli.Out(), "Successfully built %s\n", id.String())
if ref != nil {
fmt.Fprintf(dockerCli.Out(), "Successfully tagged %s\n", ref.String())
return id, nil
}

func persistInBundleStore(once *sync.Once, outWriter io.Writer, errWriter io.Writer, b *bundle.Bundle, ref reference.Reference, iidFileName string) (reference.Digested, error) {
id, err := packager.PersistInBundleStore(ref, b)
if err != nil {
return nil, err
}
return err
once.Do(func() {
fmt.Fprintf(outWriter, "Successfully built app image %s\n", id.String())
if iidFileName != "" {
if err := ioutil.WriteFile(iidFileName, []byte(id.Digest().String()), 0644); err != nil {
fmt.Fprintf(errWriter, "Failed to write App image ID in %s: %s", iidFileName, err)
}
}
})
return id, nil
}

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

func checkBuildArgsUniqueness(args []string) error {
func checkBuildArgsUniqueness(args cliOpts.ListOpts) error {
set := make(map[string]bool)
for _, value := range args {
for _, value := range args.GetAllOrEmpty() {
key := strings.Split(value, "=")[0]
if _, ok := set[key]; ok {
return fmt.Errorf("'--build-arg %s' is defined twice", key)
Expand All @@ -361,3 +399,12 @@ func checkBuildArgsUniqueness(args []string) error {
}
return nil
}

// validateTag checks if the given image name can be resolved.
func validateTag(rawRepo string) (string, error) {
_, err := reference.ParseNormalizedNamed(rawRepo)
if err != nil {
return "", err
}
return rawRepo, nil
}
2 changes: 1 addition & 1 deletion internal/commands/build/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func parseCompose(app *types.App, contextPath string, options buildOptions) (map
return nil, nil, err
}

buildArgs := buildArgsToMap(options.args)
buildArgs := buildArgsToMap(options.args.GetAllOrEmpty())

pulledServices := []compose.ServiceConfig{}
opts := map[string]build.Options{}
Expand Down
2 changes: 1 addition & 1 deletion internal/commands/build/compose_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func Test_parseCompose(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
app, err := packager.Extract("testdata/" + tt.name)
assert.NilError(t, err)
got, _, err := parseCompose(app, "testdata", buildOptions{})
got, _, err := parseCompose(app, "testdata", newBuildOptions())
assert.NilError(t, err)
_, ok := got["dontwant"]
assert.Assert(t, !ok, "parseCompose() should have excluded 'dontwant' service")
Expand Down

0 comments on commit 09d931a

Please sign in to comment.