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