github.com/fabiokung/docker@v0.11.2-0.20170222101415-4534dcd49497/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 "github.com/docker/distribution/reference" 15 "github.com/docker/docker/api" 16 "github.com/docker/docker/api/types" 17 "github.com/docker/docker/api/types/container" 18 "github.com/docker/docker/builder/dockerignore" 19 "github.com/docker/docker/cli" 20 "github.com/docker/docker/cli/command" 21 "github.com/docker/docker/cli/command/image/build" 22 "github.com/docker/docker/opts" 23 "github.com/docker/docker/pkg/archive" 24 "github.com/docker/docker/pkg/fileutils" 25 "github.com/docker/docker/pkg/jsonmessage" 26 "github.com/docker/docker/pkg/progress" 27 "github.com/docker/docker/pkg/streamformatter" 28 "github.com/docker/docker/pkg/urlutil" 29 runconfigopts "github.com/docker/docker/runconfig/opts" 30 units "github.com/docker/go-units" 31 "github.com/spf13/cobra" 32 "golang.org/x/net/context" 33 ) 34 35 type buildOptions struct { 36 context string 37 dockerfileName string 38 tags opts.ListOpts 39 labels opts.ListOpts 40 buildArgs opts.ListOpts 41 ulimits *opts.UlimitOpt 42 memory string 43 memorySwap string 44 shmSize opts.MemBytes 45 cpuShares int64 46 cpuPeriod int64 47 cpuQuota int64 48 cpuSetCpus string 49 cpuSetMems string 50 cgroupParent string 51 isolation string 52 quiet bool 53 noCache bool 54 rm bool 55 forceRm bool 56 pull bool 57 cacheFrom []string 58 compress bool 59 securityOpt []string 60 networkMode string 61 squash bool 62 } 63 64 // NewBuildCommand creates a new `docker build` command 65 func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command { 66 ulimits := make(map[string]*units.Ulimit) 67 options := buildOptions{ 68 tags: opts.NewListOpts(validateTag), 69 buildArgs: opts.NewListOpts(opts.ValidateEnv), 70 ulimits: opts.NewUlimitOpt(&ulimits), 71 labels: opts.NewListOpts(opts.ValidateEnv), 72 } 73 74 cmd := &cobra.Command{ 75 Use: "build [OPTIONS] PATH | URL | -", 76 Short: "Build an image from a Dockerfile", 77 Args: cli.ExactArgs(1), 78 RunE: func(cmd *cobra.Command, args []string) error { 79 options.context = args[0] 80 return runBuild(dockerCli, options) 81 }, 82 } 83 84 flags := cmd.Flags() 85 86 flags.VarP(&options.tags, "tag", "t", "Name and optionally a tag in the 'name:tag' format") 87 flags.Var(&options.buildArgs, "build-arg", "Set build-time variables") 88 flags.Var(options.ulimits, "ulimit", "Ulimit options") 89 flags.StringVarP(&options.dockerfileName, "file", "f", "", "Name of the Dockerfile (Default is 'PATH/Dockerfile')") 90 flags.StringVarP(&options.memory, "memory", "m", "", "Memory limit") 91 flags.StringVar(&options.memorySwap, "memory-swap", "", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap") 92 flags.Var(&options.shmSize, "shm-size", "Size of /dev/shm") 93 flags.Int64VarP(&options.cpuShares, "cpu-shares", "c", 0, "CPU shares (relative weight)") 94 flags.Int64Var(&options.cpuPeriod, "cpu-period", 0, "Limit the CPU CFS (Completely Fair Scheduler) period") 95 flags.Int64Var(&options.cpuQuota, "cpu-quota", 0, "Limit the CPU CFS (Completely Fair Scheduler) quota") 96 flags.StringVar(&options.cpuSetCpus, "cpuset-cpus", "", "CPUs in which to allow execution (0-3, 0,1)") 97 flags.StringVar(&options.cpuSetMems, "cpuset-mems", "", "MEMs in which to allow execution (0-3, 0,1)") 98 flags.StringVar(&options.cgroupParent, "cgroup-parent", "", "Optional parent cgroup for the container") 99 flags.StringVar(&options.isolation, "isolation", "", "Container isolation technology") 100 flags.Var(&options.labels, "label", "Set metadata for an image") 101 flags.BoolVar(&options.noCache, "no-cache", false, "Do not use cache when building the image") 102 flags.BoolVar(&options.rm, "rm", true, "Remove intermediate containers after a successful build") 103 flags.BoolVar(&options.forceRm, "force-rm", false, "Always remove intermediate containers") 104 flags.BoolVarP(&options.quiet, "quiet", "q", false, "Suppress the build output and print image ID on success") 105 flags.BoolVar(&options.pull, "pull", false, "Always attempt to pull a newer version of the image") 106 flags.StringSliceVar(&options.cacheFrom, "cache-from", []string{}, "Images to consider as cache sources") 107 flags.BoolVar(&options.compress, "compress", false, "Compress the build context using gzip") 108 flags.StringSliceVar(&options.securityOpt, "security-opt", []string{}, "Security options") 109 flags.StringVar(&options.networkMode, "network", "default", "Set the networking mode for the RUN instructions during build") 110 flags.SetAnnotation("network", "version", []string{"1.25"}) 111 112 command.AddTrustVerificationFlags(flags) 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 suppress 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 = build.GetContextFromReader(dockerCli.In(), options.dockerfileName) 160 case isLocalDir(specifiedContext): 161 contextDir, relDockerfile, err = build.GetContextFromLocalDir(specifiedContext, options.dockerfileName) 162 case urlutil.IsGitURL(specifiedContext): 163 tempDir, relDockerfile, err = build.GetContextFromGitURL(specifiedContext, options.dockerfileName) 164 case urlutil.IsURL(specifiedContext): 165 buildCtx, relDockerfile, err = build.GetContextFromURL(progBuff, specifiedContext, options.dockerfileName) 166 default: 167 return fmt.Errorf("unable to prepare context: path %q not found", specifiedContext) 168 } 169 170 if err != nil { 171 if options.quiet && urlutil.IsURL(specifiedContext) { 172 fmt.Fprintln(dockerCli.Err(), progBuff) 173 } 174 return fmt.Errorf("unable to prepare context: %s", err) 175 } 176 177 if tempDir != "" { 178 defer os.RemoveAll(tempDir) 179 contextDir = tempDir 180 } 181 182 if buildCtx == nil { 183 // And canonicalize dockerfile name to a platform-independent one 184 relDockerfile, err = archive.CanonicalTarNameForPath(relDockerfile) 185 if err != nil { 186 return fmt.Errorf("cannot canonicalize dockerfile path %s: %v", relDockerfile, err) 187 } 188 189 f, err := os.Open(filepath.Join(contextDir, ".dockerignore")) 190 if err != nil && !os.IsNotExist(err) { 191 return err 192 } 193 defer f.Close() 194 195 var excludes []string 196 if err == nil { 197 excludes, err = dockerignore.ReadAll(f) 198 if err != nil { 199 return err 200 } 201 } 202 203 if err := build.ValidateContextDirectory(contextDir, excludes); err != nil { 204 return fmt.Errorf("Error checking context: '%s'.", err) 205 } 206 207 // If .dockerignore mentions .dockerignore or the Dockerfile 208 // then make sure we send both files over to the daemon 209 // because Dockerfile is, obviously, needed no matter what, and 210 // .dockerignore is needed to know if either one needs to be 211 // removed. The daemon will remove them for us, if needed, after it 212 // parses the Dockerfile. Ignore errors here, as they will have been 213 // caught by validateContextDirectory above. 214 var includes = []string{"."} 215 keepThem1, _ := fileutils.Matches(".dockerignore", excludes) 216 keepThem2, _ := fileutils.Matches(relDockerfile, excludes) 217 if keepThem1 || keepThem2 { 218 includes = append(includes, ".dockerignore", relDockerfile) 219 } 220 221 compression := archive.Uncompressed 222 if options.compress { 223 compression = archive.Gzip 224 } 225 buildCtx, err = archive.TarWithOptions(contextDir, &archive.TarOptions{ 226 Compression: compression, 227 ExcludePatterns: excludes, 228 IncludeFiles: includes, 229 }) 230 if err != nil { 231 return err 232 } 233 } 234 235 ctx := context.Background() 236 237 var resolvedTags []*resolvedTag 238 if command.IsTrusted() { 239 translator := func(ctx context.Context, ref reference.NamedTagged) (reference.Canonical, error) { 240 return TrustedReference(ctx, dockerCli, ref, nil) 241 } 242 // Wrap the tar archive to replace the Dockerfile entry with the rewritten 243 // Dockerfile which uses trusted pulls. 244 buildCtx = replaceDockerfileTarWrapper(ctx, buildCtx, relDockerfile, translator, &resolvedTags) 245 } 246 247 // Setup an upload progress bar 248 progressOutput := streamformatter.NewStreamFormatter().NewProgressOutput(progBuff, true) 249 if !dockerCli.Out().IsTerminal() { 250 progressOutput = &lastProgressOutput{output: progressOutput} 251 } 252 253 var body io.Reader = progress.NewProgressReader(buildCtx, progressOutput, 0, "", "Sending build context to Docker daemon") 254 255 var memory int64 256 if options.memory != "" { 257 parsedMemory, err := units.RAMInBytes(options.memory) 258 if err != nil { 259 return err 260 } 261 memory = parsedMemory 262 } 263 264 var memorySwap int64 265 if options.memorySwap != "" { 266 if options.memorySwap == "-1" { 267 memorySwap = -1 268 } else { 269 parsedMemorySwap, err := units.RAMInBytes(options.memorySwap) 270 if err != nil { 271 return err 272 } 273 memorySwap = parsedMemorySwap 274 } 275 } 276 277 authConfigs, _ := dockerCli.GetAllCredentials() 278 buildOptions := types.ImageBuildOptions{ 279 Memory: memory, 280 MemorySwap: memorySwap, 281 Tags: options.tags.GetAll(), 282 SuppressOutput: options.quiet, 283 NoCache: options.noCache, 284 Remove: options.rm, 285 ForceRemove: options.forceRm, 286 PullParent: options.pull, 287 Isolation: container.Isolation(options.isolation), 288 CPUSetCPUs: options.cpuSetCpus, 289 CPUSetMems: options.cpuSetMems, 290 CPUShares: options.cpuShares, 291 CPUQuota: options.cpuQuota, 292 CPUPeriod: options.cpuPeriod, 293 CgroupParent: options.cgroupParent, 294 Dockerfile: relDockerfile, 295 ShmSize: options.shmSize.Value(), 296 Ulimits: options.ulimits.GetList(), 297 BuildArgs: runconfigopts.ConvertKVStringsToMapWithNil(options.buildArgs.GetAll()), 298 AuthConfigs: authConfigs, 299 Labels: runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()), 300 CacheFrom: options.cacheFrom, 301 SecurityOpt: options.securityOpt, 302 NetworkMode: options.networkMode, 303 Squash: options.squash, 304 } 305 306 response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions) 307 if err != nil { 308 if options.quiet { 309 fmt.Fprintf(dockerCli.Err(), "%s", progBuff) 310 } 311 return err 312 } 313 defer response.Body.Close() 314 315 err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, dockerCli.Out().FD(), dockerCli.Out().IsTerminal(), nil) 316 if err != nil { 317 if jerr, ok := err.(*jsonmessage.JSONError); ok { 318 // If no error code is set, default to 1 319 if jerr.Code == 0 { 320 jerr.Code = 1 321 } 322 if options.quiet { 323 fmt.Fprintf(dockerCli.Err(), "%s%s", progBuff, buildBuff) 324 } 325 return cli.StatusError{Status: jerr.Message, StatusCode: jerr.Code} 326 } 327 } 328 329 // Windows: show error message about modified file permissions if the 330 // daemon isn't running Windows. 331 if response.OSType != "windows" && runtime.GOOS == "windows" && !options.quiet { 332 fmt.Fprintln(dockerCli.Out(), `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.`) 333 } 334 335 // Everything worked so if -q was provided the output from the daemon 336 // should be just the image ID and we'll print that to stdout. 337 if options.quiet { 338 fmt.Fprintf(dockerCli.Out(), "%s", buildBuff) 339 } 340 341 if command.IsTrusted() { 342 // Since the build was successful, now we must tag any of the resolved 343 // images from the above Dockerfile rewrite. 344 for _, resolved := range resolvedTags { 345 if err := TagTrusted(ctx, dockerCli, resolved.digestRef, resolved.tagRef); err != nil { 346 return err 347 } 348 } 349 } 350 351 return nil 352 } 353 354 func isLocalDir(c string) bool { 355 _, err := os.Stat(c) 356 return err == nil 357 } 358 359 type translatorFunc func(context.Context, reference.NamedTagged) (reference.Canonical, error) 360 361 // validateTag checks if the given image name can be resolved. 362 func validateTag(rawRepo string) (string, error) { 363 _, err := reference.ParseNormalizedNamed(rawRepo) 364 if err != nil { 365 return "", err 366 } 367 368 return rawRepo, nil 369 } 370 371 var dockerfileFromLinePattern = regexp.MustCompile(`(?i)^[\s]*FROM[ \f\r\t\v]+(?P<image>[^ \f\r\t\v\n#]+)`) 372 373 // resolvedTag records the repository, tag, and resolved digest reference 374 // from a Dockerfile rewrite. 375 type resolvedTag struct { 376 digestRef reference.Canonical 377 tagRef reference.NamedTagged 378 } 379 380 // rewriteDockerfileFrom rewrites the given Dockerfile by resolving images in 381 // "FROM <image>" instructions to a digest reference. `translator` is a 382 // function that takes a repository name and tag reference and returns a 383 // trusted digest reference. 384 func rewriteDockerfileFrom(ctx context.Context, dockerfile io.Reader, translator translatorFunc) (newDockerfile []byte, resolvedTags []*resolvedTag, err error) { 385 scanner := bufio.NewScanner(dockerfile) 386 buf := bytes.NewBuffer(nil) 387 388 // Scan the lines of the Dockerfile, looking for a "FROM" line. 389 for scanner.Scan() { 390 line := scanner.Text() 391 392 matches := dockerfileFromLinePattern.FindStringSubmatch(line) 393 if matches != nil && matches[1] != api.NoBaseImageSpecifier { 394 // Replace the line with a resolved "FROM repo@digest" 395 var ref reference.Named 396 ref, err = reference.ParseNormalizedNamed(matches[1]) 397 if err != nil { 398 return nil, nil, err 399 } 400 ref = reference.TagNameOnly(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", reference.FamiliarString(trustedRef))) 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 }