github.com/olljanat/moby@v1.13.1/cli/command/image/build.go (about) 1 package image 2 3 import ( 4 "archive/tar" 5 "bufio" 6 "bytes" 7 "fmt" 8 "io" 9 "os" 10 "path/filepath" 11 "regexp" 12 "runtime" 13 14 "golang.org/x/net/context" 15 16 "github.com/docker/docker/api" 17 "github.com/docker/docker/api/types" 18 "github.com/docker/docker/api/types/container" 19 "github.com/docker/docker/builder" 20 "github.com/docker/docker/builder/dockerignore" 21 "github.com/docker/docker/cli" 22 "github.com/docker/docker/cli/command" 23 "github.com/docker/docker/opts" 24 "github.com/docker/docker/pkg/archive" 25 "github.com/docker/docker/pkg/fileutils" 26 "github.com/docker/docker/pkg/jsonmessage" 27 "github.com/docker/docker/pkg/progress" 28 "github.com/docker/docker/pkg/streamformatter" 29 "github.com/docker/docker/pkg/urlutil" 30 "github.com/docker/docker/reference" 31 runconfigopts "github.com/docker/docker/runconfig/opts" 32 "github.com/docker/go-units" 33 "github.com/spf13/cobra" 34 ) 35 36 type buildOptions struct { 37 context string 38 dockerfileName string 39 tags opts.ListOpts 40 labels opts.ListOpts 41 buildArgs opts.ListOpts 42 ulimits *runconfigopts.UlimitOpt 43 memory string 44 memorySwap string 45 shmSize string 46 cpuShares int64 47 cpuPeriod int64 48 cpuQuota int64 49 cpuSetCpus string 50 cpuSetMems string 51 cgroupParent string 52 isolation string 53 quiet bool 54 noCache bool 55 rm bool 56 forceRm bool 57 pull bool 58 cacheFrom []string 59 compress bool 60 securityOpt []string 61 networkMode string 62 squash bool 63 } 64 65 // NewBuildCommand creates a new `docker build` command 66 func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command { 67 ulimits := make(map[string]*units.Ulimit) 68 options := buildOptions{ 69 tags: opts.NewListOpts(validateTag), 70 buildArgs: opts.NewListOpts(runconfigopts.ValidateEnv), 71 ulimits: runconfigopts.NewUlimitOpt(&ulimits), 72 labels: opts.NewListOpts(runconfigopts.ValidateEnv), 73 } 74 75 cmd := &cobra.Command{ 76 Use: "build [OPTIONS] PATH | URL | -", 77 Short: "Build an image from a Dockerfile", 78 Args: cli.ExactArgs(1), 79 RunE: func(cmd *cobra.Command, args []string) error { 80 options.context = args[0] 81 return runBuild(dockerCli, options) 82 }, 83 } 84 85 flags := cmd.Flags() 86 87 flags.VarP(&options.tags, "tag", "t", "Name and optionally a tag in the 'name:tag' format") 88 flags.Var(&options.buildArgs, "build-arg", "Set build-time variables") 89 flags.Var(options.ulimits, "ulimit", "Ulimit options") 90 flags.StringVarP(&options.dockerfileName, "file", "f", "", "Name of the Dockerfile (Default is 'PATH/Dockerfile')") 91 flags.StringVarP(&options.memory, "memory", "m", "", "Memory limit") 92 flags.StringVar(&options.memorySwap, "memory-swap", "", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap") 93 flags.StringVar(&options.shmSize, "shm-size", "", "Size of /dev/shm, default value is 64MB") 94 flags.Int64VarP(&options.cpuShares, "cpu-shares", "c", 0, "CPU shares (relative weight)") 95 flags.Int64Var(&options.cpuPeriod, "cpu-period", 0, "Limit the CPU CFS (Completely Fair Scheduler) period") 96 flags.Int64Var(&options.cpuQuota, "cpu-quota", 0, "Limit the CPU CFS (Completely Fair Scheduler) quota") 97 flags.StringVar(&options.cpuSetCpus, "cpuset-cpus", "", "CPUs in which to allow execution (0-3, 0,1)") 98 flags.StringVar(&options.cpuSetMems, "cpuset-mems", "", "MEMs in which to allow execution (0-3, 0,1)") 99 flags.StringVar(&options.cgroupParent, "cgroup-parent", "", "Optional parent cgroup for the container") 100 flags.StringVar(&options.isolation, "isolation", "", "Container isolation technology") 101 flags.Var(&options.labels, "label", "Set metadata for an image") 102 flags.BoolVar(&options.noCache, "no-cache", false, "Do not use cache when building the image") 103 flags.BoolVar(&options.rm, "rm", true, "Remove intermediate containers after a successful build") 104 flags.BoolVar(&options.forceRm, "force-rm", false, "Always remove intermediate containers") 105 flags.BoolVarP(&options.quiet, "quiet", "q", false, "Suppress the build output and print image ID on success") 106 flags.BoolVar(&options.pull, "pull", false, "Always attempt to pull a newer version of the image") 107 flags.StringSliceVar(&options.cacheFrom, "cache-from", []string{}, "Images to consider as cache sources") 108 flags.BoolVar(&options.compress, "compress", false, "Compress the build context using gzip") 109 flags.StringSliceVar(&options.securityOpt, "security-opt", []string{}, "Security options") 110 flags.StringVar(&options.networkMode, "network", "default", "Set the networking mode for the RUN instructions during build") 111 112 command.AddTrustedFlags(flags, true) 113 114 flags.BoolVar(&options.squash, "squash", false, "Squash newly built layers into a single new layer") 115 flags.SetAnnotation("squash", "experimental", nil) 116 flags.SetAnnotation("squash", "version", []string{"1.25"}) 117 118 return cmd 119 } 120 121 // lastProgressOutput is the same as progress.Output except 122 // that it only output with the last update. It is used in 123 // non terminal scenarios to depresss verbose messages 124 type lastProgressOutput struct { 125 output progress.Output 126 } 127 128 // WriteProgress formats progress information from a ProgressReader. 129 func (out *lastProgressOutput) WriteProgress(prog progress.Progress) error { 130 if !prog.LastUpdate { 131 return nil 132 } 133 134 return out.output.WriteProgress(prog) 135 } 136 137 func runBuild(dockerCli *command.DockerCli, options buildOptions) error { 138 139 var ( 140 buildCtx io.ReadCloser 141 err error 142 contextDir string 143 tempDir string 144 relDockerfile string 145 progBuff io.Writer 146 buildBuff io.Writer 147 ) 148 149 specifiedContext := options.context 150 progBuff = dockerCli.Out() 151 buildBuff = dockerCli.Out() 152 if options.quiet { 153 progBuff = bytes.NewBuffer(nil) 154 buildBuff = bytes.NewBuffer(nil) 155 } 156 157 switch { 158 case specifiedContext == "-": 159 buildCtx, relDockerfile, err = builder.GetContextFromReader(dockerCli.In(), options.dockerfileName) 160 case urlutil.IsGitURL(specifiedContext): 161 tempDir, relDockerfile, err = builder.GetContextFromGitURL(specifiedContext, options.dockerfileName) 162 case urlutil.IsURL(specifiedContext): 163 buildCtx, relDockerfile, err = builder.GetContextFromURL(progBuff, specifiedContext, options.dockerfileName) 164 default: 165 contextDir, relDockerfile, err = builder.GetContextFromLocalDir(specifiedContext, options.dockerfileName) 166 } 167 168 if err != nil { 169 if options.quiet && urlutil.IsURL(specifiedContext) { 170 fmt.Fprintln(dockerCli.Err(), progBuff) 171 } 172 return fmt.Errorf("unable to prepare context: %s", err) 173 } 174 175 if tempDir != "" { 176 defer os.RemoveAll(tempDir) 177 contextDir = tempDir 178 } 179 180 if buildCtx == nil { 181 // And canonicalize dockerfile name to a platform-independent one 182 relDockerfile, err = archive.CanonicalTarNameForPath(relDockerfile) 183 if err != nil { 184 return fmt.Errorf("cannot canonicalize dockerfile path %s: %v", relDockerfile, err) 185 } 186 187 f, err := os.Open(filepath.Join(contextDir, ".dockerignore")) 188 if err != nil && !os.IsNotExist(err) { 189 return err 190 } 191 defer f.Close() 192 193 var excludes []string 194 if err == nil { 195 excludes, err = dockerignore.ReadAll(f) 196 if err != nil { 197 return err 198 } 199 } 200 201 if err := builder.ValidateContextDirectory(contextDir, excludes); err != nil { 202 return fmt.Errorf("Error checking context: '%s'.", err) 203 } 204 205 // If .dockerignore mentions .dockerignore or the Dockerfile 206 // then make sure we send both files over to the daemon 207 // because Dockerfile is, obviously, needed no matter what, and 208 // .dockerignore is needed to know if either one needs to be 209 // removed. The daemon will remove them for us, if needed, after it 210 // parses the Dockerfile. Ignore errors here, as they will have been 211 // caught by validateContextDirectory above. 212 var includes = []string{"."} 213 keepThem1, _ := fileutils.Matches(".dockerignore", excludes) 214 keepThem2, _ := fileutils.Matches(relDockerfile, excludes) 215 if keepThem1 || keepThem2 { 216 includes = append(includes, ".dockerignore", relDockerfile) 217 } 218 219 compression := archive.Uncompressed 220 if options.compress { 221 compression = archive.Gzip 222 } 223 buildCtx, err = archive.TarWithOptions(contextDir, &archive.TarOptions{ 224 Compression: compression, 225 ExcludePatterns: excludes, 226 IncludeFiles: includes, 227 }) 228 if err != nil { 229 return err 230 } 231 } 232 233 ctx := context.Background() 234 235 var resolvedTags []*resolvedTag 236 if command.IsTrusted() { 237 translator := func(ctx context.Context, ref reference.NamedTagged) (reference.Canonical, error) { 238 return TrustedReference(ctx, dockerCli, ref, nil) 239 } 240 // Wrap the tar archive to replace the Dockerfile entry with the rewritten 241 // Dockerfile which uses trusted pulls. 242 buildCtx = replaceDockerfileTarWrapper(ctx, buildCtx, relDockerfile, translator, &resolvedTags) 243 } 244 245 // Setup an upload progress bar 246 progressOutput := streamformatter.NewStreamFormatter().NewProgressOutput(progBuff, true) 247 if !dockerCli.Out().IsTerminal() { 248 progressOutput = &lastProgressOutput{output: progressOutput} 249 } 250 251 var body io.Reader = progress.NewProgressReader(buildCtx, progressOutput, 0, "", "Sending build context to Docker daemon") 252 253 var memory int64 254 if options.memory != "" { 255 parsedMemory, err := units.RAMInBytes(options.memory) 256 if err != nil { 257 return err 258 } 259 memory = parsedMemory 260 } 261 262 var memorySwap int64 263 if options.memorySwap != "" { 264 if options.memorySwap == "-1" { 265 memorySwap = -1 266 } else { 267 parsedMemorySwap, err := units.RAMInBytes(options.memorySwap) 268 if err != nil { 269 return err 270 } 271 memorySwap = parsedMemorySwap 272 } 273 } 274 275 var shmSize int64 276 if options.shmSize != "" { 277 shmSize, err = units.RAMInBytes(options.shmSize) 278 if err != nil { 279 return err 280 } 281 } 282 283 authConfigs, _ := dockerCli.GetAllCredentials() 284 buildOptions := types.ImageBuildOptions{ 285 Memory: memory, 286 MemorySwap: memorySwap, 287 Tags: options.tags.GetAll(), 288 SuppressOutput: options.quiet, 289 NoCache: options.noCache, 290 Remove: options.rm, 291 ForceRemove: options.forceRm, 292 PullParent: options.pull, 293 Isolation: container.Isolation(options.isolation), 294 CPUSetCPUs: options.cpuSetCpus, 295 CPUSetMems: options.cpuSetMems, 296 CPUShares: options.cpuShares, 297 CPUQuota: options.cpuQuota, 298 CPUPeriod: options.cpuPeriod, 299 CgroupParent: options.cgroupParent, 300 Dockerfile: relDockerfile, 301 ShmSize: shmSize, 302 Ulimits: options.ulimits.GetList(), 303 BuildArgs: runconfigopts.ConvertKVStringsToMapWithNil(options.buildArgs.GetAll()), 304 AuthConfigs: authConfigs, 305 Labels: runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()), 306 CacheFrom: options.cacheFrom, 307 SecurityOpt: options.securityOpt, 308 NetworkMode: options.networkMode, 309 Squash: options.squash, 310 } 311 312 response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions) 313 if err != nil { 314 if options.quiet { 315 fmt.Fprintf(dockerCli.Err(), "%s", progBuff) 316 } 317 return err 318 } 319 defer response.Body.Close() 320 321 err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, dockerCli.Out().FD(), dockerCli.Out().IsTerminal(), nil) 322 if err != nil { 323 if jerr, ok := err.(*jsonmessage.JSONError); ok { 324 // If no error code is set, default to 1 325 if jerr.Code == 0 { 326 jerr.Code = 1 327 } 328 if options.quiet { 329 fmt.Fprintf(dockerCli.Err(), "%s%s", progBuff, buildBuff) 330 } 331 return cli.StatusError{Status: jerr.Message, StatusCode: jerr.Code} 332 } 333 } 334 335 // Windows: show error message about modified file permissions if the 336 // daemon isn't running Windows. 337 if response.OSType != "windows" && runtime.GOOS == "windows" && !options.quiet { 338 fmt.Fprintln(dockerCli.Err(), `SECURITY WARNING: You are building a Docker image from Windows against a non-Windows Docker host. All files and directories added to build context will have '-rwxr-xr-x' permissions. It is recommended to double check and reset permissions for sensitive files and directories.`) 339 } 340 341 // Everything worked so if -q was provided the output from the daemon 342 // should be just the image ID and we'll print that to stdout. 343 if options.quiet { 344 fmt.Fprintf(dockerCli.Out(), "%s", buildBuff) 345 } 346 347 if command.IsTrusted() { 348 // Since the build was successful, now we must tag any of the resolved 349 // images from the above Dockerfile rewrite. 350 for _, resolved := range resolvedTags { 351 if err := TagTrusted(ctx, dockerCli, resolved.digestRef, resolved.tagRef); err != nil { 352 return err 353 } 354 } 355 } 356 357 return nil 358 } 359 360 type translatorFunc func(context.Context, reference.NamedTagged) (reference.Canonical, error) 361 362 // validateTag checks if the given image name can be resolved. 363 func validateTag(rawRepo string) (string, error) { 364 _, err := reference.ParseNamed(rawRepo) 365 if err != nil { 366 return "", err 367 } 368 369 return rawRepo, nil 370 } 371 372 var dockerfileFromLinePattern = regexp.MustCompile(`(?i)^[\s]*FROM[ \f\r\t\v]+(?P<image>[^ \f\r\t\v\n#]+)`) 373 374 // resolvedTag records the repository, tag, and resolved digest reference 375 // from a Dockerfile rewrite. 376 type resolvedTag struct { 377 digestRef reference.Canonical 378 tagRef reference.NamedTagged 379 } 380 381 // rewriteDockerfileFrom rewrites the given Dockerfile by resolving images in 382 // "FROM <image>" instructions to a digest reference. `translator` is a 383 // function that takes a repository name and tag reference and returns a 384 // trusted digest reference. 385 func rewriteDockerfileFrom(ctx context.Context, dockerfile io.Reader, translator translatorFunc) (newDockerfile []byte, resolvedTags []*resolvedTag, err error) { 386 scanner := bufio.NewScanner(dockerfile) 387 buf := bytes.NewBuffer(nil) 388 389 // Scan the lines of the Dockerfile, looking for a "FROM" line. 390 for scanner.Scan() { 391 line := scanner.Text() 392 393 matches := dockerfileFromLinePattern.FindStringSubmatch(line) 394 if matches != nil && matches[1] != api.NoBaseImageSpecifier { 395 // Replace the line with a resolved "FROM repo@digest" 396 ref, err := reference.ParseNamed(matches[1]) 397 if err != nil { 398 return nil, nil, err 399 } 400 ref = reference.WithDefaultTag(ref) 401 if ref, ok := ref.(reference.NamedTagged); ok && command.IsTrusted() { 402 trustedRef, err := translator(ctx, ref) 403 if err != nil { 404 return nil, nil, err 405 } 406 407 line = dockerfileFromLinePattern.ReplaceAllLiteralString(line, fmt.Sprintf("FROM %s", trustedRef.String())) 408 resolvedTags = append(resolvedTags, &resolvedTag{ 409 digestRef: trustedRef, 410 tagRef: ref, 411 }) 412 } 413 } 414 415 _, err := fmt.Fprintln(buf, line) 416 if err != nil { 417 return nil, nil, err 418 } 419 } 420 421 return buf.Bytes(), resolvedTags, scanner.Err() 422 } 423 424 // replaceDockerfileTarWrapper wraps the given input tar archive stream and 425 // replaces the entry with the given Dockerfile name with the contents of the 426 // new Dockerfile. Returns a new tar archive stream with the replaced 427 // Dockerfile. 428 func replaceDockerfileTarWrapper(ctx context.Context, inputTarStream io.ReadCloser, dockerfileName string, translator translatorFunc, resolvedTags *[]*resolvedTag) io.ReadCloser { 429 pipeReader, pipeWriter := io.Pipe() 430 go func() { 431 tarReader := tar.NewReader(inputTarStream) 432 tarWriter := tar.NewWriter(pipeWriter) 433 434 defer inputTarStream.Close() 435 436 for { 437 hdr, err := tarReader.Next() 438 if err == io.EOF { 439 // Signals end of archive. 440 tarWriter.Close() 441 pipeWriter.Close() 442 return 443 } 444 if err != nil { 445 pipeWriter.CloseWithError(err) 446 return 447 } 448 449 content := io.Reader(tarReader) 450 if hdr.Name == dockerfileName { 451 // This entry is the Dockerfile. Since the tar archive was 452 // generated from a directory on the local filesystem, the 453 // Dockerfile will only appear once in the archive. 454 var newDockerfile []byte 455 newDockerfile, *resolvedTags, err = rewriteDockerfileFrom(ctx, content, translator) 456 if err != nil { 457 pipeWriter.CloseWithError(err) 458 return 459 } 460 hdr.Size = int64(len(newDockerfile)) 461 content = bytes.NewBuffer(newDockerfile) 462 } 463 464 if err := tarWriter.WriteHeader(hdr); err != nil { 465 pipeWriter.CloseWithError(err) 466 return 467 } 468 469 if _, err := io.Copy(tarWriter, content); err != nil { 470 pipeWriter.CloseWithError(err) 471 return 472 } 473 } 474 }() 475 476 return pipeReader 477 }