diff --git a/internal/commands/build/build.go b/internal/commands/build/build.go index c6fade7a6..6a168c8ad 100644 --- a/internal/commands/build/build.go +++ b/internal/commands/build/build.go @@ -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" @@ -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" @@ -39,7 +42,7 @@ type buildOptions struct { noCache bool progress string pull bool - tag string + tags cliOpts.ListOpts folder string imageIDFile string args []string @@ -48,10 +51,12 @@ type buildOptions struct { } 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 Cmd(dockerCli command.Cli) *cobra.Command { - var opts buildOptions + opts := buildOptions{ + tags: cliOpts.NewListOpts(validateTag), + } cmd := &cobra.Command{ Use: "build [OPTIONS] BUILD_PATH", Short: "Build an App image from an App definition (.dockerapp)", @@ -66,7 +71,7 @@ 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 tagApp image tags, optionally in the 'repo: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") @@ -133,32 +138,62 @@ func runBuild(dockerCli command.Cli, contextPath string, opt buildOptions) error return err } - var ref reference.Reference - ref, err = packager.GetNamedTagged(opt.tag) - if err != nil { - return err + var out *os.File + if opt.quiet { + if out, err = os.Create(os.DevNull); err != nil { + return err + } + } else { + out = os.Stdout } - id, err := packager.PersistInBundleStore(ref, bundle) - if err != nil { - return err + var id reference.Digested + var onceWriteIIDFile sync.Once + if opt.tags.Len() == 0 { + err := opt.tags.Set("") + 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) + for _, tag := range opt.tags.GetAll() { + ref, err := packager.GetNamedTagged(tag) + if err != nil { + return err + } + id, err = persistInBundleStore(&onceWriteIIDFile, out, dockerCli.Err(), bundle, ref, opt.imageIDFile) + if err != nil { + return err + } + if tag != "" { + fmt.Fprintf(out, "Successfully tagged app %s\n", ref.String()) } } if opt.quiet { - fmt.Fprintln(dockerCli.Out(), id.Digest().String()) - return err + _, err = fmt.Fprintln(dockerCli.Out(), id.Digest().String()) + if err != nil { + return err + } + return nil } - fmt.Fprintf(dockerCli.Out(), "Successfully built %s\n", id.String()) - if ref != nil { - fmt.Fprintf(dockerCli.Out(), "Successfully tagged %s\n", ref.String()) + + return nil +} + +func persistInBundleStore(once *sync.Once, out 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(out, "Successfully built app %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) { @@ -361,3 +396,14 @@ func checkBuildArgsUniqueness(args []string) error { } return nil } + +// validateTag checks if the given image name can be resolved. +func validateTag(rawRepo string) (string, error) { + if rawRepo != "" { + _, err := reference.ParseNormalizedNamed(rawRepo) + if err != nil { + return "", err + } + } + return rawRepo, nil +}