mirror of
https://gitea.com/Lydanne/buildx.git
synced 2025-05-18 00:47:48 +08:00

This extracts the same logic for parsing annotations from the imagetools create command, and allows the same flags to be attached to the build command. These annotations are then merged into all provided exporters. Signed-off-by: Justin Chadwell <me@jedevc.com>
307 lines
7.3 KiB
Go
307 lines
7.3 KiB
Go
package commands
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/distribution/reference"
|
|
"github.com/docker/buildx/builder"
|
|
"github.com/docker/buildx/util/cobrautil/completion"
|
|
"github.com/docker/buildx/util/imagetools"
|
|
"github.com/docker/buildx/util/progress"
|
|
"github.com/docker/cli/cli/command"
|
|
"github.com/moby/buildkit/util/appcontext"
|
|
"github.com/opencontainers/go-digest"
|
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
|
"github.com/pkg/errors"
|
|
"github.com/spf13/cobra"
|
|
"golang.org/x/sync/errgroup"
|
|
)
|
|
|
|
type createOptions struct {
|
|
builder string
|
|
files []string
|
|
tags []string
|
|
annotations []string
|
|
dryrun bool
|
|
actionAppend bool
|
|
progress string
|
|
}
|
|
|
|
func runCreate(dockerCli command.Cli, in createOptions, args []string) error {
|
|
if len(args) == 0 && len(in.files) == 0 {
|
|
return errors.Errorf("no sources specified")
|
|
}
|
|
|
|
if !in.dryrun && len(in.tags) == 0 {
|
|
return errors.Errorf("can't push with no tags specified, please set --tag or --dry-run")
|
|
}
|
|
|
|
fileArgs := make([]string, len(in.files))
|
|
for i, f := range in.files {
|
|
dt, err := os.ReadFile(f)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fileArgs[i] = string(dt)
|
|
}
|
|
|
|
args = append(fileArgs, args...)
|
|
|
|
tags, err := parseRefs(in.tags)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if in.actionAppend && len(in.tags) > 0 {
|
|
args = append([]string{in.tags[0]}, args...)
|
|
}
|
|
|
|
srcs, err := parseSources(args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
repos := map[string]struct{}{}
|
|
|
|
for _, t := range tags {
|
|
repos[t.Name()] = struct{}{}
|
|
}
|
|
|
|
sourceRefs := false
|
|
for _, s := range srcs {
|
|
if s.Ref != nil {
|
|
repos[s.Ref.Name()] = struct{}{}
|
|
sourceRefs = true
|
|
}
|
|
}
|
|
|
|
if len(repos) == 0 {
|
|
return errors.Errorf("no repositories specified, please set a reference in tag or source")
|
|
}
|
|
|
|
var defaultRepo *string
|
|
if len(repos) == 1 {
|
|
for repo := range repos {
|
|
defaultRepo = &repo
|
|
}
|
|
}
|
|
|
|
for i, s := range srcs {
|
|
if s.Ref == nil {
|
|
if defaultRepo == nil {
|
|
return errors.Errorf("multiple repositories specified, cannot infer repository for %q", args[i])
|
|
}
|
|
n, err := reference.ParseNormalizedNamed(*defaultRepo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if s.Desc.MediaType == "" && s.Desc.Digest != "" {
|
|
r, err := reference.WithDigest(n, s.Desc.Digest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
srcs[i].Ref = r
|
|
sourceRefs = true
|
|
} else {
|
|
srcs[i].Ref = reference.TagNameOnly(n)
|
|
}
|
|
}
|
|
}
|
|
|
|
ctx := appcontext.Context()
|
|
|
|
b, err := builder.New(dockerCli, builder.WithName(in.builder))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
imageopt, err := b.ImageOpt()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
r := imagetools.New(imageopt)
|
|
|
|
if sourceRefs {
|
|
eg, ctx2 := errgroup.WithContext(ctx)
|
|
for i, s := range srcs {
|
|
if s.Ref == nil {
|
|
continue
|
|
}
|
|
func(i int) {
|
|
eg.Go(func() error {
|
|
_, desc, err := r.Resolve(ctx2, srcs[i].Ref.String())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if srcs[i].Desc.Digest == "" {
|
|
srcs[i].Desc = desc
|
|
} else {
|
|
var err error
|
|
srcs[i].Desc, err = mergeDesc(desc, srcs[i].Desc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}(i)
|
|
}
|
|
if err := eg.Wait(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
dt, desc, err := r.Combine(ctx, srcs, in.annotations)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if in.dryrun {
|
|
fmt.Printf("%s\n", dt)
|
|
return nil
|
|
}
|
|
|
|
// new resolver cause need new auth
|
|
r = imagetools.New(imageopt)
|
|
|
|
ctx2, cancel := context.WithCancel(context.TODO())
|
|
defer cancel()
|
|
printer, err := progress.NewPrinter(ctx2, os.Stderr, os.Stderr, in.progress)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
eg, _ := errgroup.WithContext(ctx)
|
|
pw := progress.WithPrefix(printer, "internal", true)
|
|
|
|
for _, t := range tags {
|
|
t := t
|
|
eg.Go(func() error {
|
|
return progress.Wrap(fmt.Sprintf("pushing %s", t.String()), pw.Write, func(sub progress.SubLogger) error {
|
|
eg2, _ := errgroup.WithContext(ctx)
|
|
for _, s := range srcs {
|
|
if reference.Domain(s.Ref) == reference.Domain(t) && reference.Path(s.Ref) == reference.Path(t) {
|
|
continue
|
|
}
|
|
s := s
|
|
eg2.Go(func() error {
|
|
sub.Log(1, []byte(fmt.Sprintf("copying %s from %s to %s\n", s.Desc.Digest.String(), s.Ref.String(), t.String())))
|
|
return r.Copy(ctx, s, t)
|
|
})
|
|
}
|
|
|
|
if err := eg2.Wait(); err != nil {
|
|
return err
|
|
}
|
|
sub.Log(1, []byte(fmt.Sprintf("pushing %s to %s\n", desc.Digest.String(), t.String())))
|
|
return r.Push(ctx, t, desc, dt)
|
|
})
|
|
})
|
|
}
|
|
|
|
err = eg.Wait()
|
|
err1 := printer.Wait()
|
|
if err == nil {
|
|
err = err1
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func parseSources(in []string) ([]*imagetools.Source, error) {
|
|
out := make([]*imagetools.Source, len(in))
|
|
for i, in := range in {
|
|
s, err := parseSource(in)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "failed to parse source %q, valid sources are digests, references and descriptors", in)
|
|
}
|
|
out[i] = s
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func parseRefs(in []string) ([]reference.Named, error) {
|
|
refs := make([]reference.Named, len(in))
|
|
for i, in := range in {
|
|
n, err := reference.ParseNormalizedNamed(in)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
refs[i] = n
|
|
}
|
|
return refs, nil
|
|
}
|
|
|
|
func parseSource(in string) (*imagetools.Source, error) {
|
|
// source can be a digest, reference or a descriptor JSON
|
|
dgst, err := digest.Parse(in)
|
|
if err == nil {
|
|
return &imagetools.Source{
|
|
Desc: ocispec.Descriptor{
|
|
Digest: dgst,
|
|
},
|
|
}, nil
|
|
} else if strings.HasPrefix(in, "sha256") {
|
|
return nil, err
|
|
}
|
|
|
|
ref, err := reference.ParseNormalizedNamed(in)
|
|
if err == nil {
|
|
return &imagetools.Source{
|
|
Ref: ref,
|
|
}, nil
|
|
} else if !strings.HasPrefix(in, "{") {
|
|
return nil, err
|
|
}
|
|
|
|
var s imagetools.Source
|
|
if err := json.Unmarshal([]byte(in), &s.Desc); err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
return &s, nil
|
|
}
|
|
|
|
func createCmd(dockerCli command.Cli, opts RootOptions) *cobra.Command {
|
|
var options createOptions
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "create [OPTIONS] [SOURCE] [SOURCE...]",
|
|
Short: "Create a new image based on source images",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
options.builder = *opts.Builder
|
|
return runCreate(dockerCli, options, args)
|
|
},
|
|
ValidArgsFunction: completion.Disable,
|
|
}
|
|
|
|
flags := cmd.Flags()
|
|
flags.StringArrayVarP(&options.files, "file", "f", []string{}, "Read source descriptor from file")
|
|
flags.StringArrayVarP(&options.tags, "tag", "t", []string{}, "Set reference for new image")
|
|
flags.BoolVar(&options.dryrun, "dry-run", false, "Show final image instead of pushing")
|
|
flags.BoolVar(&options.actionAppend, "append", false, "Append to existing manifest")
|
|
flags.StringVar(&options.progress, "progress", "auto", `Set type of progress output ("auto", "plain", "tty"). Use plain to show container output`)
|
|
flags.StringArrayVarP(&options.annotations, "annotation", "", []string{}, "Add annotation to the image")
|
|
|
|
return cmd
|
|
}
|
|
|
|
func mergeDesc(d1, d2 ocispec.Descriptor) (ocispec.Descriptor, error) {
|
|
if d2.Size != 0 && d1.Size != d2.Size {
|
|
return ocispec.Descriptor{}, errors.Errorf("invalid size mismatch for %s, %d != %d", d1.Digest, d2.Size, d1.Size)
|
|
}
|
|
if d2.MediaType != "" {
|
|
d1.MediaType = d2.MediaType
|
|
}
|
|
if len(d2.Annotations) != 0 {
|
|
d1.Annotations = d2.Annotations // no merge so support removes
|
|
}
|
|
if d2.Platform != nil {
|
|
d1.Platform = d2.Platform // missing items filled in later from image config
|
|
}
|
|
return d1, nil
|
|
}
|