2019-04-17 03:01:10 +08:00
package commands
import (
2022-05-23 23:42:10 +08:00
"context"
2019-04-19 05:29:02 +08:00
"encoding/json"
"fmt"
2022-03-10 08:46:06 +08:00
"os"
2019-04-19 05:29:02 +08:00
"strings"
2023-08-31 06:53:43 +08:00
"github.com/distribution/reference"
2022-12-06 02:57:35 +08:00
"github.com/docker/buildx/builder"
2024-06-21 18:56:54 +08:00
"github.com/docker/buildx/util/buildflags"
2023-04-10 03:20:18 +08:00
"github.com/docker/buildx/util/cobrautil/completion"
2019-04-25 10:29:56 +08:00
"github.com/docker/buildx/util/imagetools"
2022-05-23 23:42:10 +08:00
"github.com/docker/buildx/util/progress"
2019-04-17 03:01:10 +08:00
"github.com/docker/cli/cli/command"
2023-09-07 19:13:54 +08:00
"github.com/moby/buildkit/util/progress/progressui"
2019-04-19 05:29:02 +08:00
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
2019-04-17 03:01:10 +08:00
"github.com/pkg/errors"
"github.com/spf13/cobra"
2019-04-19 05:29:02 +08:00
"golang.org/x/sync/errgroup"
2019-04-17 03:01:10 +08:00
)
type createOptions struct {
2021-11-04 12:17:27 +08:00
builder string
2019-04-19 05:29:02 +08:00
files [ ] string
tags [ ] string
2023-07-20 22:28:52 +08:00
annotations [ ] string
2019-04-19 05:29:02 +08:00
dryrun bool
actionAppend bool
2022-05-23 23:42:10 +08:00
progress string
2024-05-30 02:07:28 +08:00
preferIndex bool
2019-04-17 03:01:10 +08:00
}
2024-01-05 20:11:53 +08:00
func runCreate ( ctx context . Context , dockerCli command . Cli , in createOptions , args [ ] string ) error {
2019-04-19 05:29:02 +08:00
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" )
}
2024-11-20 09:51:24 +08:00
fileArgs := make ( [ ] string , len ( in . files ) , len ( in . files ) + len ( args ) )
2019-04-19 05:29:02 +08:00
for i , f := range in . files {
2022-03-10 08:46:06 +08:00
dt , err := os . ReadFile ( f )
2019-04-19 05:29:02 +08:00
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" )
}
2022-05-10 22:23:26 +08:00
var defaultRepo * string
if len ( repos ) == 1 {
for repo := range repos {
defaultRepo = & repo
}
2019-04-19 05:29:02 +08:00
}
for i , s := range srcs {
2022-12-01 06:18:43 +08:00
if s . Ref == nil {
2022-05-10 22:23:26 +08:00
if defaultRepo == nil {
2022-05-18 18:52:28 +08:00
return errors . Errorf ( "multiple repositories specified, cannot infer repository for %q" , args [ i ] )
2022-05-10 22:23:26 +08:00
}
n , err := reference . ParseNormalizedNamed ( * defaultRepo )
2019-04-19 05:29:02 +08:00
if err != nil {
return err
}
2022-12-01 06:18:43 +08:00
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 )
2019-04-19 05:29:02 +08:00
}
}
}
2022-12-06 02:57:35 +08:00
b , err := builder . New ( dockerCli , builder . WithName ( in . builder ) )
2021-11-04 12:17:27 +08:00
if err != nil {
return err
}
2022-12-06 02:57:35 +08:00
imageopt , err := b . ImageOpt ( )
2021-11-04 12:17:27 +08:00
if err != nil {
return err
}
r := imagetools . New ( imageopt )
2019-04-19 05:29:02 +08:00
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
}
2021-04-10 01:49:15 +08:00
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
}
}
2019-04-19 05:29:02 +08:00
return nil
} )
} ( i )
}
if err := eg . Wait ( ) ; err != nil {
return err
}
}
2024-06-21 18:56:54 +08:00
annotations , err := buildflags . ParseAnnotations ( in . annotations )
if err != nil {
return errors . Wrapf ( err , "failed to parse annotations" )
}
dt , desc , err := r . Combine ( ctx , srcs , annotations , in . preferIndex )
2019-04-19 05:29:02 +08:00
if err != nil {
return err
}
if in . dryrun {
fmt . Printf ( "%s\n" , dt )
2019-04-19 11:07:00 +08:00
return nil
}
// new resolver cause need new auth
2021-11-04 12:17:27 +08:00
r = imagetools . New ( imageopt )
2019-04-19 11:07:00 +08:00
2024-11-20 10:21:44 +08:00
ctx2 , cancel := context . WithCancelCause ( context . TODO ( ) )
defer func ( ) { cancel ( errors . WithStack ( context . Canceled ) ) } ( )
2023-09-07 19:13:54 +08:00
printer , err := progress . NewPrinter ( ctx2 , os . Stderr , progressui . DisplayMode ( in . progress ) )
2022-10-25 17:55:36 +08:00
if err != nil {
return err
}
2022-05-23 23:42:10 +08:00
eg , _ := errgroup . WithContext ( ctx )
pw := progress . WithPrefix ( printer , "internal" , true )
2019-04-19 11:07:00 +08:00
for _ , t := range tags {
2022-05-23 23:42:10 +08:00
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 )
} )
} )
}
2022-05-18 18:52:28 +08:00
2022-05-23 23:42:10 +08:00
err = eg . Wait ( )
err1 := printer . Wait ( )
if err == nil {
err = err1
2019-04-19 05:29:02 +08:00
}
2022-05-23 23:42:10 +08:00
return err
2019-04-19 05:29:02 +08:00
}
2022-05-10 22:23:26 +08:00
func parseSources ( in [ ] string ) ( [ ] * imagetools . Source , error ) {
out := make ( [ ] * imagetools . Source , len ( in ) )
2019-04-19 05:29:02 +08:00
for i , in := range in {
s , err := parseSource ( in )
if err != nil {
2021-02-17 15:42:08 +08:00
return nil , errors . Wrapf ( err , "failed to parse source %q, valid sources are digests, references and descriptors" , in )
2019-04-19 05:29:02 +08:00
}
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
}
2022-05-10 22:23:26 +08:00
func parseSource ( in string ) ( * imagetools . Source , error ) {
2019-04-19 05:29:02 +08:00
// source can be a digest, reference or a descriptor JSON
dgst , err := digest . Parse ( in )
if err == nil {
2022-05-10 22:23:26 +08:00
return & imagetools . Source {
2019-04-19 05:29:02 +08:00
Desc : ocispec . Descriptor {
Digest : dgst ,
} ,
} , nil
} else if strings . HasPrefix ( in , "sha256" ) {
return nil , err
}
ref , err := reference . ParseNormalizedNamed ( in )
if err == nil {
2022-05-10 22:23:26 +08:00
return & imagetools . Source {
2019-04-19 05:29:02 +08:00
Ref : ref ,
} , nil
} else if ! strings . HasPrefix ( in , "{" ) {
return nil , err
}
2022-05-10 22:23:26 +08:00
var s imagetools . Source
2019-04-19 05:29:02 +08:00
if err := json . Unmarshal ( [ ] byte ( in ) , & s . Desc ) ; err != nil {
return nil , errors . WithStack ( err )
}
return & s , nil
2019-04-17 03:01:10 +08:00
}
2021-11-04 12:17:27 +08:00
func createCmd ( dockerCli command . Cli , opts RootOptions ) * cobra . Command {
2019-04-17 03:01:10 +08:00
var options createOptions
cmd := & cobra . Command {
2019-04-19 05:29:02 +08:00
Use : "create [OPTIONS] [SOURCE] [SOURCE...]" ,
2019-04-17 03:01:10 +08:00
Short : "Create a new image based on source images" ,
2024-01-20 08:44:30 +08:00
RunE : func ( cmd * cobra . Command , args [ ] string ) error {
2022-04-13 21:36:48 +08:00
options . builder = * opts . Builder
2024-01-05 20:11:53 +08:00
return runCreate ( cmd . Context ( ) , dockerCli , options , args )
2024-01-20 08:44:30 +08:00
} ,
2023-04-10 03:20:18 +08:00
ValidArgsFunction : completion . Disable ,
2019-04-17 03:01:10 +08:00
}
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" )
2019-04-19 05:29:02 +08:00
flags . BoolVar ( & options . actionAppend , "append" , false , "Append to existing manifest" )
2024-06-04 22:53:27 +08:00
flags . StringVar ( & options . progress , "progress" , "auto" , ` Set type of progress output ("auto", "plain", "tty", "rawjson"). Use plain to show container output ` )
2023-07-20 22:28:52 +08:00
flags . StringArrayVarP ( & options . annotations , "annotation" , "" , [ ] string { } , "Add annotation to the image" )
2024-05-30 02:07:28 +08:00
flags . BoolVar ( & options . preferIndex , "prefer-index" , true , "When only a single source is specified, prefer outputting an image index or manifest list instead of performing a carbon copy" )
2019-04-17 03:01:10 +08:00
return cmd
}
2021-04-10 01:49:15 +08:00
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
}