github.com/kim0/docker@v0.6.2-0.20161130212042-4addda3f07e7/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 } 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(runconfigopts.ValidateArg), 70 ulimits: runconfigopts.NewUlimitOpt(&ulimits), 71 labels: opts.NewListOpts(runconfigopts.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", "Connect a container to a network") 110 111 command.AddTrustedFlags(flags, true) 112 113 return cmd 114 } 115 116 // lastProgressOutput is the same as progress.Output except 117 // that it only output with the last update. It is used in 118 // non terminal scenarios to depresss verbose messages 119 type lastProgressOutput struct { 120 output progress.Output 121 } 122 123 // WriteProgress formats progress information from a ProgressReader. 124 func (out *lastProgressOutput) WriteProgress(prog progress.Progress) error { 125 if !prog.LastUpdate { 126 return nil 127 } 128 129 return out.output.WriteProgress(prog) 130 } 131 132 func runBuild(dockerCli *command.DockerCli, options buildOptions) error { 133 134 var ( 135 buildCtx io.ReadCloser 136 err error 137 ) 138 139 specifiedContext := options.context 140 141 var ( 142 contextDir string 143 tempDir string 144 relDockerfile string 145 progBuff io.Writer 146 buildBuff io.Writer 147 ) 148 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 = builder.GetContextFromReader(dockerCli.In(), options.dockerfileName) 159 case urlutil.IsGitURL(specifiedContext): 160 tempDir, relDockerfile, err = builder.GetContextFromGitURL(specifiedContext, options.dockerfileName) 161 case urlutil.IsURL(specifiedContext): 162 buildCtx, relDockerfile, err = builder.GetContextFromURL(progBuff, specifiedContext, options.dockerfileName) 163 default: 164 contextDir, relDockerfile, err = builder.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 := builder.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) 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 authConfig, _ := dockerCli.CredentialsStore().GetAll() 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.ConvertKVStringsToMap(options.buildArgs.GetAll()), 303 AuthConfigs: authConfig, 304 Labels: runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()), 305 CacheFrom: options.cacheFrom, 306 SecurityOpt: options.securityOpt, 307 NetworkMode: options.networkMode, 308 } 309 310 response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions) 311 if err != nil { 312 if options.quiet { 313 fmt.Fprintf(dockerCli.Err(), "%s", progBuff) 314 } 315 return err 316 } 317 defer response.Body.Close() 318 319 err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, dockerCli.Out().FD(), dockerCli.Out().IsTerminal(), nil) 320 if err != nil { 321 if jerr, ok := err.(*jsonmessage.JSONError); ok { 322 // If no error code is set, default to 1 323 if jerr.Code == 0 { 324 jerr.Code = 1 325 } 326 if options.quiet { 327 fmt.Fprintf(dockerCli.Err(), "%s%s", progBuff, buildBuff) 328 } 329 return cli.StatusError{Status: jerr.Message, StatusCode: jerr.Code} 330 } 331 } 332 333 // Windows: show error message about modified file permissions if the 334 // daemon isn't running Windows. 335 if response.OSType != "windows" && runtime.GOOS == "windows" && !options.quiet { 336 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.`) 337 } 338 339 // Everything worked so if -q was provided the output from the daemon 340 // should be just the image ID and we'll print that to stdout. 341 if options.quiet { 342 fmt.Fprintf(dockerCli.Out(), "%s", buildBuff) 343 } 344 345 if command.IsTrusted() { 346 // Since the build was successful, now we must tag any of the resolved 347 // images from the above Dockerfile rewrite. 348 for _, resolved := range resolvedTags { 349 if err := TagTrusted(ctx, dockerCli, resolved.digestRef, resolved.tagRef); err != nil { 350 return err 351 } 352 } 353 } 354 355 return nil 356 } 357 358 type translatorFunc func(context.Context, reference.NamedTagged) (reference.Canonical, error) 359 360 // validateTag checks if the given image name can be resolved. 361 func validateTag(rawRepo string) (string, error) { 362 _, err := reference.ParseNamed(rawRepo) 363 if err != nil { 364 return "", err 365 } 366 367 return rawRepo, nil 368 } 369 370 var dockerfileFromLinePattern = regexp.MustCompile(`(?i)^[\s]*FROM[ \f\r\t\v]+(?P<image>[^ \f\r\t\v\n#]+)`) 371 372 // resolvedTag records the repository, tag, and resolved digest reference 373 // from a Dockerfile rewrite. 374 type resolvedTag struct { 375 digestRef reference.Canonical 376 tagRef reference.NamedTagged 377 } 378 379 // rewriteDockerfileFrom rewrites the given Dockerfile by resolving images in 380 // "FROM <image>" instructions to a digest reference. `translator` is a 381 // function that takes a repository name and tag reference and returns a 382 // trusted digest reference. 383 func rewriteDockerfileFrom(ctx context.Context, dockerfile io.Reader, translator translatorFunc) (newDockerfile []byte, resolvedTags []*resolvedTag, err error) { 384 scanner := bufio.NewScanner(dockerfile) 385 buf := bytes.NewBuffer(nil) 386 387 // Scan the lines of the Dockerfile, looking for a "FROM" line. 388 for scanner.Scan() { 389 line := scanner.Text() 390 391 matches := dockerfileFromLinePattern.FindStringSubmatch(line) 392 if matches != nil && matches[1] != api.NoBaseImageSpecifier { 393 // Replace the line with a resolved "FROM repo@digest" 394 ref, err := reference.ParseNamed(matches[1]) 395 if err != nil { 396 return nil, nil, err 397 } 398 ref = reference.WithDefaultTag(ref) 399 if ref, ok := ref.(reference.NamedTagged); ok && command.IsTrusted() { 400 trustedRef, err := translator(ctx, ref) 401 if err != nil { 402 return nil, nil, err 403 } 404 405 line = dockerfileFromLinePattern.ReplaceAllLiteralString(line, fmt.Sprintf("FROM %s", trustedRef.String())) 406 resolvedTags = append(resolvedTags, &resolvedTag{ 407 digestRef: trustedRef, 408 tagRef: ref, 409 }) 410 } 411 } 412 413 _, err := fmt.Fprintln(buf, line) 414 if err != nil { 415 return nil, nil, err 416 } 417 } 418 419 return buf.Bytes(), resolvedTags, scanner.Err() 420 } 421 422 // replaceDockerfileTarWrapper wraps the given input tar archive stream and 423 // replaces the entry with the given Dockerfile name with the contents of the 424 // new Dockerfile. Returns a new tar archive stream with the replaced 425 // Dockerfile. 426 func replaceDockerfileTarWrapper(ctx context.Context, inputTarStream io.ReadCloser, dockerfileName string, translator translatorFunc, resolvedTags *[]*resolvedTag) io.ReadCloser { 427 pipeReader, pipeWriter := io.Pipe() 428 go func() { 429 tarReader := tar.NewReader(inputTarStream) 430 tarWriter := tar.NewWriter(pipeWriter) 431 432 defer inputTarStream.Close() 433 434 for { 435 hdr, err := tarReader.Next() 436 if err == io.EOF { 437 // Signals end of archive. 438 tarWriter.Close() 439 pipeWriter.Close() 440 return 441 } 442 if err != nil { 443 pipeWriter.CloseWithError(err) 444 return 445 } 446 447 content := io.Reader(tarReader) 448 if hdr.Name == dockerfileName { 449 // This entry is the Dockerfile. Since the tar archive was 450 // generated from a directory on the local filesystem, the 451 // Dockerfile will only appear once in the archive. 452 var newDockerfile []byte 453 newDockerfile, *resolvedTags, err = rewriteDockerfileFrom(ctx, content, translator) 454 if err != nil { 455 pipeWriter.CloseWithError(err) 456 return 457 } 458 hdr.Size = int64(len(newDockerfile)) 459 content = bytes.NewBuffer(newDockerfile) 460 } 461 462 if err := tarWriter.WriteHeader(hdr); err != nil { 463 pipeWriter.CloseWithError(err) 464 return 465 } 466 467 if _, err := io.Copy(tarWriter, content); err != nil { 468 pipeWriter.CloseWithError(err) 469 return 470 } 471 } 472 }() 473 474 return pipeReader 475 }