github.com/docker/app@v0.9.1-beta3.0.20210611140623-a48f773ab002/internal/commands/build/build.go (about)

     1  package build
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"os"
    11  	"path/filepath"
    12  	"strconv"
    13  	"strings"
    14  	"sync"
    15  
    16  	"github.com/docker/app/internal"
    17  	"github.com/docker/app/internal/packager"
    18  	"github.com/docker/app/types"
    19  
    20  	"github.com/containerd/console"
    21  	"github.com/deislabs/cnab-go/bundle"
    22  	cnab "github.com/deislabs/cnab-go/driver"
    23  	"github.com/docker/buildx/build"
    24  	"github.com/docker/buildx/driver"
    25  	_ "github.com/docker/buildx/driver/docker" // required to get default driver registered, see driver/docker/factory.go:14
    26  	"github.com/docker/buildx/util/progress"
    27  	"github.com/docker/cli/cli"
    28  	"github.com/docker/cli/cli/command"
    29  	compose "github.com/docker/cli/cli/compose/types"
    30  	"github.com/docker/cli/cli/streams"
    31  	cliOpts "github.com/docker/cli/opts"
    32  	"github.com/docker/cnab-to-oci/remotes"
    33  	"github.com/docker/distribution/reference"
    34  	"github.com/moby/buildkit/client"
    35  	"github.com/moby/buildkit/session"
    36  	"github.com/moby/buildkit/session/auth/authprovider"
    37  	"github.com/moby/buildkit/util/appcontext"
    38  	"github.com/pkg/errors"
    39  	"github.com/sirupsen/logrus"
    40  	"github.com/spf13/cobra"
    41  )
    42  
    43  type buildOptions struct {
    44  	noCache        bool
    45  	progress       string
    46  	pull           bool
    47  	tags           cliOpts.ListOpts
    48  	folder         string
    49  	imageIDFile    string
    50  	args           cliOpts.ListOpts
    51  	quiet          bool
    52  	noResolveImage bool
    53  }
    54  
    55  const buildExample = `- $ docker app build .
    56  - $ docker app build --file myapp.dockerapp --tag myrepo/myapp:1.0.0 --tag myrepo/myapp:latest .`
    57  
    58  func newBuildOptions() buildOptions {
    59  	return buildOptions{
    60  		tags: cliOpts.NewListOpts(validateTag),
    61  		args: cliOpts.NewListOpts(nil),
    62  	}
    63  }
    64  
    65  func Cmd(dockerCli command.Cli) *cobra.Command {
    66  	opts := newBuildOptions()
    67  	cmd := &cobra.Command{
    68  		Use:     "build [OPTIONS] BUILD_PATH",
    69  		Short:   "Build an App image from an App definition (.dockerapp)",
    70  		Example: buildExample,
    71  		Args:    cli.ExactArgs(1),
    72  		RunE: func(cmd *cobra.Command, args []string) error {
    73  			return runBuild(dockerCli, args[0], opts)
    74  		},
    75  	}
    76  
    77  	flags := cmd.Flags()
    78  	flags.BoolVar(&opts.noCache, "no-cache", false, "Do not use cache when building the App image")
    79  	flags.StringVar(&opts.progress, "progress", "auto", "Set type of progress output (auto, plain, tty). Use plain to show container output")
    80  	flags.BoolVar(&opts.noResolveImage, "no-resolve-image", false, "Do not query the registry to resolve image digest")
    81  	flags.VarP(&opts.tags, "tag", "t", "Name and optionally a tag in the 'name:tag' format")
    82  	flags.StringVarP(&opts.folder, "file", "f", "", "App definition as a .dockerapp directory")
    83  	flags.BoolVar(&opts.pull, "pull", false, "Always attempt to pull a newer version of the App image")
    84  	flags.Var(&opts.args, "build-arg", "Set build-time variables")
    85  	flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress the build output and print App image ID on success")
    86  	flags.StringVar(&opts.imageIDFile, "iidfile", "", "Write the App image ID to the file")
    87  
    88  	return cmd
    89  }
    90  
    91  type File struct {
    92  	f *streams.Out
    93  }
    94  
    95  func NewFile(f *streams.Out) console.File {
    96  	return File{f: f}
    97  }
    98  
    99  func (f File) Fd() uintptr {
   100  	return f.f.FD()
   101  }
   102  
   103  func (f File) Name() string {
   104  	return os.Stdout.Name()
   105  }
   106  
   107  func (f File) Read(p []byte) (n int, err error) {
   108  	return 0, nil
   109  }
   110  
   111  func (f File) Write(p []byte) (n int, err error) {
   112  	return f.f.Write(p)
   113  }
   114  
   115  func (f File) Close() error {
   116  	return nil
   117  }
   118  
   119  func getOutputFile(realOut *streams.Out, quiet bool) (console.File, error) {
   120  	if quiet {
   121  		nullFile, err := os.Create(os.DevNull)
   122  		if err != nil {
   123  			return nil, err
   124  		}
   125  		return nullFile, nil
   126  	}
   127  	return NewFile(realOut), nil
   128  }
   129  
   130  func runBuild(dockerCli command.Cli, contextPath string, opt buildOptions) error {
   131  	err := checkMinimalEngineVersion(dockerCli)
   132  	if err != nil {
   133  		return err
   134  	}
   135  
   136  	if opt.imageIDFile != "" {
   137  		// Avoid leaving a stale file if we eventually fail
   138  		if err := os.Remove(opt.imageIDFile); err != nil && !os.IsNotExist(err) {
   139  			return err
   140  		}
   141  	}
   142  
   143  	if err = checkBuildArgsUniqueness(opt.args); err != nil {
   144  		return err
   145  	}
   146  
   147  	application, err := getAppFolder(opt, contextPath)
   148  	if err != nil {
   149  		return err
   150  	}
   151  
   152  	app, err := packager.Extract(application)
   153  	if err != nil {
   154  		return err
   155  	}
   156  	defer app.Cleanup()
   157  
   158  	bndl, err := buildImageUsingBuildx(app, contextPath, opt, dockerCli)
   159  	if err != nil {
   160  		return err
   161  	}
   162  
   163  	out, err := getOutputFile(dockerCli.Out(), opt.quiet)
   164  	if err != nil {
   165  		return err
   166  	}
   167  
   168  	id, err := persistTags(bndl, opt.tags, opt.imageIDFile, out, dockerCli.Err())
   169  	if err != nil {
   170  		return err
   171  	}
   172  
   173  	if opt.quiet {
   174  		_, err = fmt.Fprintln(dockerCli.Out(), id.Digest().String())
   175  	}
   176  	return err
   177  }
   178  
   179  func persistTags(bndl *bundle.Bundle, tags cliOpts.ListOpts, iidFile string, outWriter io.Writer, errWriter io.Writer) (reference.Digested, error) {
   180  	var (
   181  		id               reference.Digested
   182  		onceWriteIIDFile sync.Once
   183  	)
   184  	if tags.Len() == 0 {
   185  		return persistInImageStore(&onceWriteIIDFile, outWriter, errWriter, bndl, nil, iidFile)
   186  	}
   187  	for _, tag := range tags.GetAll() {
   188  		ref, err := packager.GetNamedTagged(tag)
   189  		if err != nil {
   190  			return nil, err
   191  		}
   192  		id, err = persistInImageStore(&onceWriteIIDFile, outWriter, errWriter, bndl, ref, iidFile)
   193  		if err != nil {
   194  			return nil, err
   195  		}
   196  		if tag != "" {
   197  			fmt.Fprintf(outWriter, "Successfully tagged app image %s\n", ref.String())
   198  		}
   199  	}
   200  	return id, nil
   201  }
   202  
   203  func persistInImageStore(once *sync.Once, outWriter io.Writer, errWriter io.Writer, b *bundle.Bundle, ref reference.Reference, iidFileName string) (reference.Digested, error) {
   204  	id, err := packager.PersistInImageStore(ref, b)
   205  	if err != nil {
   206  		return nil, err
   207  	}
   208  	once.Do(func() {
   209  		fmt.Fprintf(outWriter, "Successfully built app image %s\n", id.String())
   210  		if iidFileName != "" {
   211  			if err := ioutil.WriteFile(iidFileName, []byte(id.Digest().String()), 0644); err != nil {
   212  				fmt.Fprintf(errWriter, "Failed to write App image ID in %s: %s", iidFileName, err)
   213  			}
   214  		}
   215  	})
   216  	return id, nil
   217  }
   218  
   219  func buildImageUsingBuildx(app *types.App, contextPath string, opt buildOptions, dockerCli command.Cli) (*bundle.Bundle, error) {
   220  	buildopts, pulledServices, err := parseCompose(app, contextPath, opt)
   221  	if err != nil {
   222  		return nil, err
   223  	}
   224  	buildopts["com.docker.app.invocation-image"], err = createInvocationImageBuildOptions(dockerCli, app)
   225  	if err != nil {
   226  		return nil, err
   227  	}
   228  	debugBuildOpts(buildopts)
   229  	ctx, cancel := context.WithCancel(appcontext.Context())
   230  	defer cancel()
   231  	const drivername = "buildx_buildkit_default"
   232  	d, err := driver.GetDriver(ctx, drivername, nil, dockerCli.Client(), nil, nil, "", nil, "")
   233  	if err != nil {
   234  		return nil, err
   235  	}
   236  	driverInfo := []build.DriverInfo{
   237  		{
   238  			Name:   "default",
   239  			Driver: d,
   240  		},
   241  	}
   242  
   243  	out, err := getOutputFile(dockerCli.Out(), opt.quiet)
   244  	if err != nil {
   245  		return nil, err
   246  	}
   247  
   248  	pw := progress.NewPrinter(ctx, out, opt.progress)
   249  
   250  	// We rely on buildx "docker" builder integrated in docker engine, so don't need a DockerAPI here
   251  	resp, err := build.Build(ctx, driverInfo, buildopts, nil, dockerCli.ConfigFile(), pw)
   252  	if err != nil {
   253  		return nil, err
   254  	}
   255  
   256  	bundle, err := packager.MakeBundleFromApp(dockerCli, app, nil)
   257  	if err != nil {
   258  		return nil, err
   259  	}
   260  	err = updateBundle(dockerCli, bundle, resp)
   261  	if err != nil {
   262  		return nil, err
   263  	}
   264  
   265  	if !opt.noResolveImage {
   266  		if err = fixServiceImageReferences(ctx, dockerCli, bundle, pulledServices); err != nil {
   267  			return nil, err
   268  		}
   269  	}
   270  
   271  	return bundle, nil
   272  }
   273  
   274  func fixServiceImageReferences(ctx context.Context, dockerCli command.Cli, bundle *bundle.Bundle, pulledServices []compose.ServiceConfig) error {
   275  	insecureRegistries, err := internal.InsecureRegistriesFromEngine(dockerCli)
   276  	if err != nil {
   277  		return errors.Wrapf(err, "could not retrieve insecure registries")
   278  	}
   279  	resolver := remotes.CreateResolver(dockerCli.ConfigFile(), insecureRegistries...)
   280  	for _, service := range pulledServices {
   281  		image := bundle.Images[service.Name]
   282  		ref, err := reference.ParseDockerRef(service.Image)
   283  		if err != nil {
   284  			return errors.Wrapf(err, "could not resolve image %s", service.Image)
   285  		}
   286  		_, desc, err := resolver.Resolve(ctx, ref.String())
   287  		if err != nil {
   288  			return errors.Wrapf(err, "could not resolve image %s", ref.Name())
   289  		}
   290  		canonical, err := reference.WithDigest(ref, desc.Digest)
   291  		if err != nil {
   292  			return errors.Wrapf(err, "could not resolve image %s", ref.Name())
   293  		}
   294  		image.Image = canonical.String()
   295  		bundle.Images[service.Name] = image
   296  	}
   297  	return nil
   298  }
   299  
   300  func getAppFolder(opt buildOptions, contextPath string) (string, error) {
   301  	application := opt.folder
   302  	if application == "" {
   303  		files, err := ioutil.ReadDir(contextPath)
   304  		if err != nil {
   305  			return "", err
   306  		}
   307  		for _, f := range files {
   308  			if strings.HasSuffix(f.Name(), ".dockerapp") {
   309  				if application != "" {
   310  					return "", fmt.Errorf("%s contains multiple .dockerapp directories, use -f option to select the App definition to build", contextPath)
   311  				}
   312  				application = filepath.Join(contextPath, f.Name())
   313  				if !f.IsDir() {
   314  					return "", fmt.Errorf("%s isn't a directory", f.Name())
   315  				}
   316  			}
   317  		}
   318  	}
   319  	return application, nil
   320  }
   321  
   322  func checkMinimalEngineVersion(dockerCli command.Cli) error {
   323  	info, err := dockerCli.Client().Info(appcontext.Context())
   324  	if err != nil {
   325  		return err
   326  	}
   327  	majorVersion, err := strconv.Atoi(strings.SplitN(info.ServerVersion, ".", 2)[0])
   328  	if err != nil {
   329  		return err
   330  	}
   331  	if majorVersion < 19 {
   332  		return fmt.Errorf("'build' require docker engine 19.03 or later")
   333  	}
   334  	return nil
   335  }
   336  
   337  func updateBundle(dockerCli command.Cli, bundle *bundle.Bundle, resp map[string]*client.SolveResponse) error {
   338  	debugSolveResponses(resp)
   339  	for service, r := range resp {
   340  		digest := r.ExporterResponse["containerimage.digest"]
   341  		inspect, _, err := dockerCli.Client().ImageInspectWithRaw(context.TODO(), digest)
   342  		if err != nil {
   343  			return err
   344  		}
   345  		size := uint64(inspect.Size)
   346  		if service == "com.docker.app.invocation-image" {
   347  			bundle.InvocationImages[0].Digest = digest
   348  			bundle.InvocationImages[0].Size = size
   349  		} else {
   350  			image := bundle.Images[service]
   351  			image.ImageType = cnab.ImageTypeDocker
   352  			image.Digest = digest
   353  			image.Size = size
   354  			bundle.Images[service] = image
   355  		}
   356  	}
   357  	debugBundle(bundle)
   358  	return nil
   359  }
   360  
   361  func createInvocationImageBuildOptions(dockerCli command.Cli, app *types.App) (build.Options, error) {
   362  	buildContext := bytes.NewBuffer(nil)
   363  	if err := packager.PackInvocationImageContext(dockerCli, app, buildContext); err != nil {
   364  		return build.Options{}, err
   365  	}
   366  	return build.Options{
   367  		Inputs: build.Inputs{
   368  			InStream:    buildContext,
   369  			ContextPath: "-",
   370  		},
   371  		Session: []session.Attachable{authprovider.NewDockerAuthProvider(os.Stderr)},
   372  	}, nil
   373  }
   374  
   375  func debugBuildOpts(opts map[string]build.Options) {
   376  	if logrus.IsLevelEnabled(logrus.DebugLevel) {
   377  		dt, err := json.MarshalIndent(opts, "  > ", "   ")
   378  		if err != nil {
   379  			logrus.Debugf("Failed to marshal Bundle: %s", err.Error())
   380  		} else {
   381  			logrus.Debug(string(dt))
   382  		}
   383  	}
   384  }
   385  
   386  func debugBundle(bundle *bundle.Bundle) {
   387  	if logrus.IsLevelEnabled(logrus.DebugLevel) {
   388  		dt, err := json.MarshalIndent(bundle, "  > ", "   ")
   389  		if err != nil {
   390  			logrus.Debugf("Failed to marshal Bundle: %s", err.Error())
   391  		} else {
   392  			logrus.Debug(string(dt))
   393  		}
   394  	}
   395  }
   396  
   397  func debugSolveResponses(resp map[string]*client.SolveResponse) {
   398  	if logrus.IsLevelEnabled(logrus.DebugLevel) {
   399  		dt, err := json.MarshalIndent(resp, "  > ", "   ")
   400  		if err != nil {
   401  			logrus.Debugf("Failed to marshal Buildx response: %s", err.Error())
   402  		} else {
   403  			logrus.Debug(string(dt))
   404  		}
   405  	}
   406  }
   407  
   408  func checkBuildArgsUniqueness(args cliOpts.ListOpts) error {
   409  	set := make(map[string]bool)
   410  	for _, value := range args.GetAllOrEmpty() {
   411  		key := strings.Split(value, "=")[0]
   412  		if _, ok := set[key]; ok {
   413  			return fmt.Errorf("'--build-arg %s' is defined twice", key)
   414  		}
   415  		set[key] = true
   416  	}
   417  	return nil
   418  }
   419  
   420  // validateTag checks if the given image name can be resolved.
   421  func validateTag(rawRepo string) (string, error) {
   422  	_, err := reference.ParseNormalizedNamed(rawRepo)
   423  	if err != nil {
   424  		return "", err
   425  	}
   426  	return rawRepo, nil
   427  }