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