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 }