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