github.com/flavio/docker@v0.1.3-0.20170117145210-f63d1a6eec47/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/docker/api" 15 "github.com/docker/docker/api/types" 16 "github.com/docker/docker/api/types/container" 17 "github.com/docker/docker/builder/dockerignore" 18 "github.com/docker/docker/cli" 19 "github.com/docker/docker/cli/command" 20 "github.com/docker/docker/cli/command/image/build" 21 "github.com/docker/docker/opts" 22 "github.com/docker/docker/pkg/archive" 23 "github.com/docker/docker/pkg/fileutils" 24 "github.com/docker/docker/pkg/jsonmessage" 25 "github.com/docker/docker/pkg/progress" 26 "github.com/docker/docker/pkg/streamformatter" 27 "github.com/docker/docker/pkg/urlutil" 28 "github.com/docker/docker/reference" 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 string 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.StringVar(&options.shmSize, "shm-size", "", "Size of /dev/shm, default value is 64MB") 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 111 command.AddTrustedFlags(flags, true) 112 113 flags.BoolVar(&options.squash, "squash", false, "Squash newly built layers into a single new layer") 114 flags.SetAnnotation("squash", "experimental", nil) 115 flags.SetAnnotation("squash", "version", []string{"1.25"}) 116 117 return cmd 118 } 119 120 // lastProgressOutput is the same as progress.Output except 121 // that it only output with the last update. It is used in 122 // non terminal scenarios to depresss verbose messages 123 type lastProgressOutput struct { 124 output progress.Output 125 } 126 127 // WriteProgress formats progress information from a ProgressReader. 128 func (out *lastProgressOutput) WriteProgress(prog progress.Progress) error { 129 if !prog.LastUpdate { 130 return nil 131 } 132 133 return out.output.WriteProgress(prog) 134 } 135 136 func runBuild(dockerCli *command.DockerCli, options buildOptions) error { 137 138 var ( 139 buildCtx io.ReadCloser 140 err error 141 contextDir string 142 tempDir string 143 relDockerfile string 144 progBuff io.Writer 145 buildBuff io.Writer 146 ) 147 148 specifiedContext := options.context 149 progBuff = dockerCli.Out() 150 buildBuff = dockerCli.Out() 151 if options.quiet { 152 progBuff = bytes.NewBuffer(nil) 153 buildBuff = bytes.NewBuffer(nil) 154 } 155 156 switch { 157 case specifiedContext == "-": 158 buildCtx, relDockerfile, err = build.GetContextFromReader(dockerCli.In(), options.dockerfileName) 159 case urlutil.IsGitURL(specifiedContext): 160 tempDir, relDockerfile, err = build.GetContextFromGitURL(specifiedContext, options.dockerfileName) 161 case urlutil.IsURL(specifiedContext): 162 buildCtx, relDockerfile, err = build.GetContextFromURL(progBuff, specifiedContext, options.dockerfileName) 163 default: 164 contextDir, relDockerfile, err = build.GetContextFromLocalDir(specifiedContext, options.dockerfileName) 165 } 166 167 if err != nil { 168 if options.quiet && urlutil.IsURL(specifiedContext) { 169 fmt.Fprintln(dockerCli.Err(), progBuff) 170 } 171 return fmt.Errorf("unable to prepare context: %s", err) 172 } 173 174 if tempDir != "" { 175 defer os.RemoveAll(tempDir) 176 contextDir = tempDir 177 } 178 179 if buildCtx == nil { 180 // And canonicalize dockerfile name to a platform-independent one 181 relDockerfile, err = archive.CanonicalTarNameForPath(relDockerfile) 182 if err != nil { 183 return fmt.Errorf("cannot canonicalize dockerfile path %s: %v", relDockerfile, err) 184 } 185 186 f, err := os.Open(filepath.Join(contextDir, ".dockerignore")) 187 if err != nil && !os.IsNotExist(err) { 188 return err 189 } 190 defer f.Close() 191 192 var excludes []string 193 if err == nil { 194 excludes, err = dockerignore.ReadAll(f) 195 if err != nil { 196 return err 197 } 198 } 199 200 if err := build.ValidateContextDirectory(contextDir, excludes); err != nil { 201 return fmt.Errorf("Error checking context: '%s'.", err) 202 } 203 204 // If .dockerignore mentions .dockerignore or the Dockerfile 205 // then make sure we send both files over to the daemon 206 // because Dockerfile is, obviously, needed no matter what, and 207 // .dockerignore is needed to know if either one needs to be 208 // removed. The daemon will remove them for us, if needed, after it 209 // parses the Dockerfile. Ignore errors here, as they will have been 210 // caught by validateContextDirectory above. 211 var includes = []string{"."} 212 keepThem1, _ := fileutils.Matches(".dockerignore", excludes) 213 keepThem2, _ := fileutils.Matches(relDockerfile, excludes) 214 if keepThem1 || keepThem2 { 215 includes = append(includes, ".dockerignore", relDockerfile) 216 } 217 218 compression := archive.Uncompressed 219 if options.compress { 220 compression = archive.Gzip 221 } 222 buildCtx, err = archive.TarWithOptions(contextDir, &archive.TarOptions{ 223 Compression: compression, 224 ExcludePatterns: excludes, 225 IncludeFiles: includes, 226 }) 227 if err != nil { 228 return err 229 } 230 } 231 232 ctx := context.Background() 233 234 var resolvedTags []*resolvedTag 235 if command.IsTrusted() { 236 translator := func(ctx context.Context, ref reference.NamedTagged) (reference.Canonical, error) { 237 return TrustedReference(ctx, dockerCli, ref, nil) 238 } 239 // Wrap the tar archive to replace the Dockerfile entry with the rewritten 240 // Dockerfile which uses trusted pulls. 241 buildCtx = replaceDockerfileTarWrapper(ctx, buildCtx, relDockerfile, translator, &resolvedTags) 242 } 243 244 // Setup an upload progress bar 245 progressOutput := streamformatter.NewStreamFormatter().NewProgressOutput(progBuff, true) 246 if !dockerCli.Out().IsTerminal() { 247 progressOutput = &lastProgressOutput{output: progressOutput} 248 } 249 250 var body io.Reader = progress.NewProgressReader(buildCtx, progressOutput, 0, "", "Sending build context to Docker daemon") 251 252 var memory int64 253 if options.memory != "" { 254 parsedMemory, err := units.RAMInBytes(options.memory) 255 if err != nil { 256 return err 257 } 258 memory = parsedMemory 259 } 260 261 var memorySwap int64 262 if options.memorySwap != "" { 263 if options.memorySwap == "-1" { 264 memorySwap = -1 265 } else { 266 parsedMemorySwap, err := units.RAMInBytes(options.memorySwap) 267 if err != nil { 268 return err 269 } 270 memorySwap = parsedMemorySwap 271 } 272 } 273 274 var shmSize int64 275 if options.shmSize != "" { 276 shmSize, err = units.RAMInBytes(options.shmSize) 277 if err != nil { 278 return err 279 } 280 } 281 282 authConfigs, _ := dockerCli.GetAllCredentials() 283 buildOptions := types.ImageBuildOptions{ 284 Memory: memory, 285 MemorySwap: memorySwap, 286 Tags: options.tags.GetAll(), 287 SuppressOutput: options.quiet, 288 NoCache: options.noCache, 289 Remove: options.rm, 290 ForceRemove: options.forceRm, 291 PullParent: options.pull, 292 Isolation: container.Isolation(options.isolation), 293 CPUSetCPUs: options.cpuSetCpus, 294 CPUSetMems: options.cpuSetMems, 295 CPUShares: options.cpuShares, 296 CPUQuota: options.cpuQuota, 297 CPUPeriod: options.cpuPeriod, 298 CgroupParent: options.cgroupParent, 299 Dockerfile: relDockerfile, 300 ShmSize: shmSize, 301 Ulimits: options.ulimits.GetList(), 302 BuildArgs: runconfigopts.ConvertKVStringsToMapWithNil(options.buildArgs.GetAll()), 303 AuthConfigs: authConfigs, 304 Labels: runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()), 305 CacheFrom: options.cacheFrom, 306 SecurityOpt: options.securityOpt, 307 NetworkMode: options.networkMode, 308 Squash: options.squash, 309 } 310 311 response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions) 312 if err != nil { 313 if options.quiet { 314 fmt.Fprintf(dockerCli.Err(), "%s", progBuff) 315 } 316 return err 317 } 318 defer response.Body.Close() 319 320 err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, dockerCli.Out().FD(), dockerCli.Out().IsTerminal(), nil) 321 if err != nil { 322 if jerr, ok := err.(*jsonmessage.JSONError); ok { 323 // If no error code is set, default to 1 324 if jerr.Code == 0 { 325 jerr.Code = 1 326 } 327 if options.quiet { 328 fmt.Fprintf(dockerCli.Err(), "%s%s", progBuff, buildBuff) 329 } 330 return cli.StatusError{Status: jerr.Message, StatusCode: jerr.Code} 331 } 332 } 333 334 // Windows: show error message about modified file permissions if the 335 // daemon isn't running Windows. 336 if response.OSType != "windows" && runtime.GOOS == "windows" && !options.quiet { 337 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.`) 338 } 339 340 // Everything worked so if -q was provided the output from the daemon 341 // should be just the image ID and we'll print that to stdout. 342 if options.quiet { 343 fmt.Fprintf(dockerCli.Out(), "%s", buildBuff) 344 } 345 346 if command.IsTrusted() { 347 // Since the build was successful, now we must tag any of the resolved 348 // images from the above Dockerfile rewrite. 349 for _, resolved := range resolvedTags { 350 if err := TagTrusted(ctx, dockerCli, resolved.digestRef, resolved.tagRef); err != nil { 351 return err 352 } 353 } 354 } 355 356 return 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.ParseNamed(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 ref, err := reference.ParseNamed(matches[1]) 396 if err != nil { 397 return nil, nil, err 398 } 399 ref = reference.WithDefaultTag(ref) 400 if ref, ok := ref.(reference.NamedTagged); ok && command.IsTrusted() { 401 trustedRef, err := translator(ctx, ref) 402 if err != nil { 403 return nil, nil, err 404 } 405 406 line = dockerfileFromLinePattern.ReplaceAllLiteralString(line, fmt.Sprintf("FROM %s", trustedRef.String())) 407 resolvedTags = append(resolvedTags, &resolvedTag{ 408 digestRef: trustedRef, 409 tagRef: ref, 410 }) 411 } 412 } 413 414 _, err := fmt.Fprintln(buf, line) 415 if err != nil { 416 return nil, nil, err 417 } 418 } 419 420 return buf.Bytes(), resolvedTags, scanner.Err() 421 } 422 423 // replaceDockerfileTarWrapper wraps the given input tar archive stream and 424 // replaces the entry with the given Dockerfile name with the contents of the 425 // new Dockerfile. Returns a new tar archive stream with the replaced 426 // Dockerfile. 427 func replaceDockerfileTarWrapper(ctx context.Context, inputTarStream io.ReadCloser, dockerfileName string, translator translatorFunc, resolvedTags *[]*resolvedTag) io.ReadCloser { 428 pipeReader, pipeWriter := io.Pipe() 429 go func() { 430 tarReader := tar.NewReader(inputTarStream) 431 tarWriter := tar.NewWriter(pipeWriter) 432 433 defer inputTarStream.Close() 434 435 for { 436 hdr, err := tarReader.Next() 437 if err == io.EOF { 438 // Signals end of archive. 439 tarWriter.Close() 440 pipeWriter.Close() 441 return 442 } 443 if err != nil { 444 pipeWriter.CloseWithError(err) 445 return 446 } 447 448 content := io.Reader(tarReader) 449 if hdr.Name == dockerfileName { 450 // This entry is the Dockerfile. Since the tar archive was 451 // generated from a directory on the local filesystem, the 452 // Dockerfile will only appear once in the archive. 453 var newDockerfile []byte 454 newDockerfile, *resolvedTags, err = rewriteDockerfileFrom(ctx, content, translator) 455 if err != nil { 456 pipeWriter.CloseWithError(err) 457 return 458 } 459 hdr.Size = int64(len(newDockerfile)) 460 content = bytes.NewBuffer(newDockerfile) 461 } 462 463 if err := tarWriter.WriteHeader(hdr); err != nil { 464 pipeWriter.CloseWithError(err) 465 return 466 } 467 468 if _, err := io.Copy(tarWriter, content); err != nil { 469 pipeWriter.CloseWithError(err) 470 return 471 } 472 } 473 }() 474 475 return pipeReader 476 }