// Reads a Dockerfile from the current context. It assumes that the // 'filename' is a relative path from the root of the context func (b *Builder) readDockerfile(origFile string) error { filename, err := symlink.FollowSymlinkInScope(filepath.Join(b.contextPath, origFile), b.contextPath) if err != nil { return fmt.Errorf("The Dockerfile (%s) must be within the build context", origFile) } fi, err := os.Lstat(filename) if os.IsNotExist(err) { return fmt.Errorf("Cannot locate specified Dockerfile: %s", origFile) } if fi.Size() == 0 { return ErrDockerfileEmpty } f, err := os.Open(filename) if err != nil { return err } b.dockerfile, err = parser.Parse(f) f.Close() if err != nil { return err } // After the Dockerfile has been parsed, we need to check the .dockerignore // file for either "Dockerfile" or ".dockerignore", and if either are // present then erase them from the build context. These files should never // have been sent from the client but we did send them to make sure that // we had the Dockerfile to actually parse, and then we also need the // .dockerignore file to know whether either file should be removed. // Note that this assumes the Dockerfile has been read into memory and // is now safe to be removed. excludes, _ := utils.ReadDockerIgnore(filepath.Join(b.contextPath, ".dockerignore")) if rm, _ := fileutils.Matches(".dockerignore", excludes); rm == true { os.Remove(filepath.Join(b.contextPath, ".dockerignore")) b.context.(tarsum.BuilderContext).Remove(".dockerignore") } if rm, _ := fileutils.Matches(b.dockerfileName, excludes); rm == true { os.Remove(filepath.Join(b.contextPath, b.dockerfileName)) b.context.(tarsum.BuilderContext).Remove(b.dockerfileName) } return nil }
// getArchive returns the tarfile io.ReadCloser. It is a direct copy of the // logic found in the official docker client. // See <https://github.com/docker/docker/blob/78f2b8d8/api/client/build.go#L126-L172>. func getArchive(contextDir, relDockerfile string) (io.ReadCloser, error) { var err error // And canonicalize dockerfile name to a platform-independent one relDockerfile, err = archive.CanonicalTarNameForPath(relDockerfile) if err != nil { return nil, fmt.Errorf("cannot canonicalize dockerfile path %s: %v", relDockerfile, err) } f, err := os.Open(filepath.Join(contextDir, ".dockerignore")) if err != nil && !os.IsNotExist(err) { return nil, err } var excludes []string if err == nil { excludes, err = dockerignore.ReadAll(f) if err != nil { return nil, err } } // If .dockerignore mentions .dockerignore or the Dockerfile // then make sure we send both files over to the daemon // because Dockerfile is, obviously, needed no matter what, and // .dockerignore is needed to know if either one needs to be // removed. The daemon will remove them for us, if needed, after it // parses the Dockerfile. Ignore errors here, as they will have been // caught by validateContextDirectory above. var includes = []string{"."} keepThem1, _ := fileutils.Matches(".dockerignore", excludes) keepThem2, _ := fileutils.Matches(relDockerfile, excludes) if keepThem1 || keepThem2 { includes = append(includes, ".dockerignore", relDockerfile) } return archive.TarWithOptions(contextDir, &archive.TarOptions{ Compression: archive.Uncompressed, ExcludePatterns: excludes, IncludeFiles: includes, }) }
func checkLargeFiles() error { title := "Large files in <file>.dockerignore</file>" startCheck(title) files := map[string]int64{} message := "" di, _ := readDockerIgnore(".") f := func(path string, info os.FileInfo, err error) error { m, err := fileutils.Matches(path, di) if err != nil { return err } if m { return nil } if info.Size() >= 200000000 { files[path] = info.Size() } return nil } err := filepath.Walk(".", f) if err != nil { return err } if len(files) == 0 { diagnose(Diagnosis{ Title: title, Kind: "success", }) return nil } for k, v := range files { message += fmt.Sprintf( "<warning>./%s is %d bytes, perhaps you should add it to your .dockerignore to speed up builds and deploys</warning>\n", k, v, ) } diagnose(Diagnosis{ Title: title, Kind: "warning", DocsLink: "https://docs.docker.com/engine/reference/builder/#/dockerignore-file", Description: message, }) return nil }
func send(ch chan Change, op, file, base string, ignore []string) { rel, err := filepath.Rel(base, file) if err != nil { return } if match, _ := fileutils.Matches(rel, ignore); match { return } change := Change{ Operation: op, Base: base, Path: rel, } ch <- change }
// validateContextDirectory checks if all the contents of the directory // can be read and returns an error if some files can't be read // symlinks which point to non-existing files don't trigger an error func validateContextDirectory(srcPath string, excludes []string) error { contextRoot, err := getContextRoot(srcPath) if err != nil { return err } return filepath.Walk(contextRoot, func(filePath string, f os.FileInfo, err error) error { // skip this directory/file if it's not in the path, it won't get added to the context if relFilePath, err := filepath.Rel(contextRoot, filePath); err != nil { return err } else if skip, err := fileutils.Matches(relFilePath, excludes); err != nil { return err } else if skip { if f.IsDir() { return filepath.SkipDir } return nil } if err != nil { if os.IsPermission(err) { return fmt.Errorf("can't stat '%s'", filePath) } if os.IsNotExist(err) { return nil } return err } // skip checking if symlinks point to non-existing files, such symlinks can be useful // also skip named pipes, because they hanging on open if f.Mode()&(os.ModeSymlink|os.ModeNamedPipe) != 0 { return nil } if !f.IsDir() { currentFile, err := os.Open(filePath) if err != nil && os.IsPermission(err) { return fmt.Errorf("no permission to read from '%s'", filePath) } currentFile.Close() } return nil }) }
func createTarStream(srcPath, dockerfilePath string) (io.ReadCloser, error) { excludes, err := parseDockerignore(srcPath) if err != nil { return nil, err } includes := []string{"."} // If .dockerignore mentions .dockerignore or the Dockerfile // then make sure we send both files over to the daemon // because Dockerfile is, obviously, needed no matter what, and // .dockerignore is needed to know if either one needs to be // removed. The deamon will remove them for us, if needed, after it // parses the Dockerfile. // // https://github.com/docker/docker/issues/8330 // forceIncludeFiles := []string{".dockerignore", dockerfilePath} for _, includeFile := range forceIncludeFiles { if includeFile == "" { continue } keepThem, err := fileutils.Matches(includeFile, excludes) if err != nil { return nil, fmt.Errorf("cannot match .dockerfile: '%s', error: %s", includeFile, err) } if keepThem { includes = append(includes, includeFile) } } if err := validateContextDirectory(srcPath, excludes); err != nil { return nil, err } tarOpts := &archive.TarOptions{ ExcludePatterns: excludes, IncludeFiles: includes, Compression: archive.Uncompressed, NoLchown: true, } return archive.TarWithOptions(srcPath, tarOpts) }
// Process reads the .dockerignore file at the root of the embedded context. // If .dockerignore does not exist in the context, then nil is returned. // // It can take a list of files to be removed after .dockerignore is removed. // This is used for server-side implementations of builders that need to send // the .dockerignore file as well as the special files specified in filesToRemove, // but expect them to be excluded from the context after they were processed. // // For example, server-side Dockerfile builders are expected to pass in the name // of the Dockerfile to be removed after it was parsed. // // TODO: Don't require a ModifiableContext (use Context instead) and don't remove // files, instead handle a list of files to be excluded from the context. func (c DockerIgnoreContext) Process(filesToRemove []string) error { dockerignore, err := c.Open(".dockerignore") // Note that a missing .dockerignore file isn't treated as an error if err != nil { if os.IsNotExist(err) { return nil } return err } excludes, _ := utils.ReadDockerIgnore(dockerignore) filesToRemove = append([]string{".dockerignore"}, filesToRemove...) for _, fileToRemove := range filesToRemove { rm, _ := fileutils.Matches(fileToRemove, excludes) if rm { c.Remove(fileToRemove) } } return nil }
func indexWalker(root string, index client.Index, ignore []string) filepath.WalkFunc { return func(path string, info os.FileInfo, err error) error { rel, err := filepath.Rel(root, path) if err != nil { return err } if info.IsDir() { return nil } match, err := fileutils.Matches(rel, ignore) if err != nil { return err } if match { return nil } data, err := ioutil.ReadFile(path) if err != nil { return err } sum := sha256.Sum256(data) hash := hex.EncodeToString([]byte(sum[:])) index[hash] = client.IndexItem{ Name: rel, Mode: info.Mode(), ModTime: info.ModTime(), Size: len(data), } return nil } }
// TarWithOptions creates an archive from the directory at `path`, only including files whose relative // paths are included in `options.IncludeFiles` (if non-nil) or not in `options.ExcludePatterns`. func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error) { pipeReader, pipeWriter := io.Pipe() compressWriter, err := CompressStream(pipeWriter, options.Compression) if err != nil { return nil, err } go func() { ta := &tarAppender{ TarWriter: tar.NewWriter(compressWriter), Buffer: pools.BufioWriter32KPool.Get(nil), SeenFiles: make(map[uint64]string), } // this buffer is needed for the duration of this piped stream defer pools.BufioWriter32KPool.Put(ta.Buffer) // In general we log errors here but ignore them because // during e.g. a diff operation the container can continue // mutating the filesystem and we can see transient errors // from this if options.IncludeFiles == nil { options.IncludeFiles = []string{"."} } seen := make(map[string]bool) var renamedRelFilePath string // For when tar.Options.Name is set for _, include := range options.IncludeFiles { filepath.Walk(filepath.Join(srcPath, include), func(filePath string, f os.FileInfo, err error) error { if err != nil { logrus.Debugf("Tar: Can't stat file %s to tar: %s", srcPath, err) return nil } relFilePath, err := filepath.Rel(srcPath, filePath) if err != nil || (relFilePath == "." && f.IsDir()) { // Error getting relative path OR we are looking // at the root path. Skip in both situations. return nil } skip := false // If "include" is an exact match for the current file // then even if there's an "excludePatterns" pattern that // matches it, don't skip it. IOW, assume an explicit 'include' // is asking for that file no matter what - which is true // for some files, like .dockerignore and Dockerfile (sometimes) if include != relFilePath { skip, err = fileutils.Matches(relFilePath, options.ExcludePatterns) if err != nil { logrus.Debugf("Error matching %s", relFilePath, err) return err } } if skip { if f.IsDir() { return filepath.SkipDir } return nil } if seen[relFilePath] { return nil } seen[relFilePath] = true // Rename the base resource if options.Name != "" && filePath == srcPath+"/"+filepath.Base(relFilePath) { renamedRelFilePath = relFilePath } // Set this to make sure the items underneath also get renamed if options.Name != "" { relFilePath = strings.Replace(relFilePath, renamedRelFilePath, options.Name, 1) } if err := ta.addTarFile(filePath, relFilePath); err != nil { logrus.Debugf("Can't add file %s to tar: %s", filePath, err) } return nil }) } // Make sure to check the error on Close. if err := ta.TarWriter.Close(); err != nil { logrus.Debugf("Can't close tar writer: %s", err) } if err := compressWriter.Close(); err != nil { logrus.Debugf("Can't close compress writer: %s", err) } if err := pipeWriter.Close(); err != nil { logrus.Debugf("Can't close pipe writer: %s", err) } }() return pipeReader, nil }
// CmdBuild builds a new image from the source code at a given path. // // If '-' is provided instead of a path or URL, Docker will build an image from either a Dockerfile or tar archive read from STDIN. // // Usage: docker build [OPTIONS] PATH | URL | - func (cli *DockerCli) CmdBuild(args ...string) error { cmd := cli.Subcmd("build", "PATH | URL | -", "Build a new image from the source code at PATH", true) tag := cmd.String([]string{"t", "-tag"}, "", "Repository name (and optionally a tag) for the image") suppressOutput := cmd.Bool([]string{"q", "-quiet"}, false, "Suppress the verbose output generated by the containers") noCache := cmd.Bool([]string{"#no-cache", "-no-cache"}, false, "Do not use cache when building the image") rm := cmd.Bool([]string{"#rm", "-rm"}, true, "Remove intermediate containers after a successful build") forceRm := cmd.Bool([]string{"-force-rm"}, false, "Always remove intermediate containers") pull := cmd.Bool([]string{"-pull"}, false, "Always attempt to pull a newer version of the image") dockerfileName := cmd.String([]string{"f", "-file"}, "", "Name of the Dockerfile (Default is 'PATH/Dockerfile')") flMemoryString := cmd.String([]string{"m", "-memory"}, "", "Memory limit") flMemorySwap := cmd.String([]string{"-memory-swap"}, "", "Total memory (memory + swap), '-1' to disable swap") flCPUShares := cmd.Int64([]string{"c", "-cpu-shares"}, 0, "CPU shares (relative weight)") flCpuQuota := cmd.Int64([]string{"-cpu-quota"}, 0, "Limit the CPU CFS (Completely Fair Scheduler) quota") flCPUSetCpus := cmd.String([]string{"-cpuset-cpus"}, "", "CPUs in which to allow execution (0-3, 0,1)") flCPUSetMems := cmd.String([]string{"-cpuset-mems"}, "", "MEMs in which to allow execution (0-3, 0,1)") flCgroupParent := cmd.String([]string{"-cgroup-parent"}, "", "Optional parent cgroup for the container") cmd.Require(flag.Exact, 1) cmd.ParseFlags(args, true) var ( context archive.Archive isRemote bool err error ) _, err = exec.LookPath("git") hasGit := err == nil if cmd.Arg(0) == "-" { // As a special case, 'docker build -' will build from either an empty context with the // contents of stdin as a Dockerfile, or a tar-ed context from stdin. buf := bufio.NewReader(cli.in) magic, err := buf.Peek(tarHeaderSize) if err != nil && err != io.EOF { return fmt.Errorf("failed to peek context header from STDIN: %v", err) } if !archive.IsArchive(magic) { dockerfile, err := ioutil.ReadAll(buf) if err != nil { return fmt.Errorf("failed to read Dockerfile from STDIN: %v", err) } // -f option has no meaning when we're reading it from stdin, // so just use our default Dockerfile name *dockerfileName = api.DefaultDockerfileName context, err = archive.Generate(*dockerfileName, string(dockerfile)) } else { context = ioutil.NopCloser(buf) } } else if urlutil.IsURL(cmd.Arg(0)) && (!urlutil.IsGitURL(cmd.Arg(0)) || !hasGit) { isRemote = true } else { root := cmd.Arg(0) if urlutil.IsGitURL(root) { root, err = utils.GitClone(root) if err != nil { return err } defer os.RemoveAll(root) } if _, err := os.Stat(root); err != nil { return err } absRoot, err := filepath.Abs(root) if err != nil { return err } filename := *dockerfileName // path to Dockerfile if *dockerfileName == "" { // No -f/--file was specified so use the default *dockerfileName = api.DefaultDockerfileName filename = filepath.Join(absRoot, *dockerfileName) // Just to be nice ;-) look for 'dockerfile' too but only // use it if we found it, otherwise ignore this check if _, err = os.Lstat(filename); os.IsNotExist(err) { tmpFN := path.Join(absRoot, strings.ToLower(*dockerfileName)) if _, err = os.Lstat(tmpFN); err == nil { *dockerfileName = strings.ToLower(*dockerfileName) filename = tmpFN } } } origDockerfile := *dockerfileName // used for error msg if filename, err = filepath.Abs(filename); err != nil { return err } // Verify that 'filename' is within the build context filename, err = symlink.FollowSymlinkInScope(filename, absRoot) if err != nil { return fmt.Errorf("The Dockerfile (%s) must be within the build context (%s)", origDockerfile, root) } // Now reset the dockerfileName to be relative to the build context *dockerfileName, err = filepath.Rel(absRoot, filename) if err != nil { return err } // And canonicalize dockerfile name to a platform-independent one *dockerfileName, err = archive.CanonicalTarNameForPath(*dockerfileName) if err != nil { return fmt.Errorf("Cannot canonicalize dockerfile path %s: %v", *dockerfileName, err) } if _, err = os.Lstat(filename); os.IsNotExist(err) { return fmt.Errorf("Cannot locate Dockerfile: %s", origDockerfile) } var includes = []string{"."} excludes, err := utils.ReadDockerIgnore(path.Join(root, ".dockerignore")) if err != nil { return err } // If .dockerignore mentions .dockerignore or the Dockerfile // then make sure we send both files over to the daemon // because Dockerfile is, obviously, needed no matter what, and // .dockerignore is needed to know if either one needs to be // removed. The deamon will remove them for us, if needed, after it // parses the Dockerfile. keepThem1, _ := fileutils.Matches(".dockerignore", excludes) keepThem2, _ := fileutils.Matches(*dockerfileName, excludes) if keepThem1 || keepThem2 { includes = append(includes, ".dockerignore", *dockerfileName) } if err := utils.ValidateContextDirectory(root, excludes); err != nil { return fmt.Errorf("Error checking context is accessible: '%s'. Please check permissions and try again.", err) } options := &archive.TarOptions{ Compression: archive.Uncompressed, ExcludePatterns: excludes, IncludeFiles: includes, } context, err = archive.TarWithOptions(root, options) if err != nil { return err } } // windows: show error message about modified file permissions // FIXME: this is not a valid warning when the daemon is running windows. should be removed once docker engine for windows can build. if runtime.GOOS == "windows" { logrus.Warn(`SECURITY WARNING: You are building a Docker image from Windows against a Linux 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.`) } var body io.Reader // Setup an upload progress bar // FIXME: ProgressReader shouldn't be this annoying to use if context != nil { sf := streamformatter.NewStreamFormatter(false) body = progressreader.New(progressreader.Config{ In: context, Out: cli.out, Formatter: sf, NewLines: true, ID: "", Action: "Sending build context to Docker daemon", }) } var memory int64 if *flMemoryString != "" { parsedMemory, err := units.RAMInBytes(*flMemoryString) if err != nil { return err } memory = parsedMemory } var memorySwap int64 if *flMemorySwap != "" { if *flMemorySwap == "-1" { memorySwap = -1 } else { parsedMemorySwap, err := units.RAMInBytes(*flMemorySwap) if err != nil { return err } memorySwap = parsedMemorySwap } } // Send the build context v := &url.Values{} //Check if the given image name can be resolved if *tag != "" { repository, tag := parsers.ParseRepositoryTag(*tag) if err := registry.ValidateRepositoryName(repository); err != nil { return err } if len(tag) > 0 { if err := graph.ValidateTagName(tag); err != nil { return err } } } v.Set("t", *tag) if *suppressOutput { v.Set("q", "1") } if isRemote { v.Set("remote", cmd.Arg(0)) } if *noCache { v.Set("nocache", "1") } if *rm { v.Set("rm", "1") } else { v.Set("rm", "0") } if *forceRm { v.Set("forcerm", "1") } if *pull { v.Set("pull", "1") } v.Set("cpusetcpus", *flCPUSetCpus) v.Set("cpusetmems", *flCPUSetMems) v.Set("cpushares", strconv.FormatInt(*flCPUShares, 10)) v.Set("cpuquota", strconv.FormatInt(*flCpuQuota, 10)) v.Set("memory", strconv.FormatInt(memory, 10)) v.Set("memswap", strconv.FormatInt(memorySwap, 10)) v.Set("cgroupparent", *flCgroupParent) v.Set("dockerfile", *dockerfileName) headers := http.Header(make(map[string][]string)) buf, err := json.Marshal(cli.configFile.AuthConfigs) if err != nil { return err } headers.Add("X-Registry-Config", base64.URLEncoding.EncodeToString(buf)) if context != nil { headers.Set("Content-Type", "application/tar") } sopts := &streamOpts{ rawTerminal: true, in: body, out: cli.out, headers: headers, } err = cli.stream("POST", fmt.Sprintf("/build?%s", v.Encode()), sopts) if jerr, ok := err.(*jsonmessage.JSONError); ok { // If no error code is set, default to 1 if jerr.Code == 0 { jerr.Code = 1 } return StatusError{Status: jerr.Message, StatusCode: jerr.Code} } return err }
func watchForChanges(files map[string]map[string]time.Time, dir string, ignore []string, ch chan Change) error { for { for file, _ := range files[dir] { if _, err := os.Stat(file); os.IsNotExist(err) { rel, err := filepath.Rel(dir, file) if err != nil { return err } delete(files[dir], file) ch <- Change{ Operation: "remove", Base: dir, Path: rel, } } } filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { if path == dir { return nil } if info.IsDir() { return nil } e, ok := files[dir][path] if !ok || e.Before(info.ModTime()) { rel, err := filepath.Rel(dir, path) if err != nil { return err } match, err := fileutils.Matches(rel, ignore) if err != nil { return err } if match { return nil } files[dir][path] = info.ModTime() ch <- Change{ Operation: "add", Base: dir, Path: rel, } } return nil }) time.Sleep(700 * time.Millisecond) } }
// CmdBuild builds a new image from the source code at a given path. // // If '-' is provided instead of a path or URL, Docker will build an image from either a Dockerfile or tar archive read from STDIN. // // Usage: docker build [OPTIONS] PATH | URL | - func (cli *DockerCli) CmdBuild(args ...string) error { cmd := Cli.Subcmd("build", []string{"PATH | URL | -"}, Cli.DockerCommands["build"].Description, true) flTags := opts.NewListOpts(validateTag) cmd.Var(&flTags, []string{"t", "-tag"}, "Name and optionally a tag in the 'name:tag' format") suppressOutput := cmd.Bool([]string{"q", "-quiet"}, false, "Suppress the verbose output generated by the containers") noCache := cmd.Bool([]string{"#no-cache", "-no-cache"}, false, "Do not use cache when building the image") rm := cmd.Bool([]string{"#rm", "-rm"}, true, "Remove intermediate containers after a successful build") forceRm := cmd.Bool([]string{"-force-rm"}, false, "Always remove intermediate containers") pull := cmd.Bool([]string{"-pull"}, false, "Always attempt to pull a newer version of the image") dockerfileName := cmd.String([]string{"f", "-file"}, "", "Name of the Dockerfile (Default is 'PATH/Dockerfile')") flMemoryString := cmd.String([]string{"m", "-memory"}, "", "Memory limit") flMemorySwap := cmd.String([]string{"-memory-swap"}, "", "Total memory (memory + swap), '-1' to disable swap") flCPUShares := cmd.Int64([]string{"#c", "-cpu-shares"}, 0, "CPU shares (relative weight)") flCPUPeriod := cmd.Int64([]string{"-cpu-period"}, 0, "Limit the CPU CFS (Completely Fair Scheduler) period") flCPUQuota := cmd.Int64([]string{"-cpu-quota"}, 0, "Limit the CPU CFS (Completely Fair Scheduler) quota") flCPUSetCpus := cmd.String([]string{"-cpuset-cpus"}, "", "CPUs in which to allow execution (0-3, 0,1)") flCPUSetMems := cmd.String([]string{"-cpuset-mems"}, "", "MEMs in which to allow execution (0-3, 0,1)") flCgroupParent := cmd.String([]string{"-cgroup-parent"}, "", "Optional parent cgroup for the container") flBuildArg := opts.NewListOpts(opts.ValidateEnv) cmd.Var(&flBuildArg, []string{"-build-arg"}, "Set build-time variables") ulimits := make(map[string]*ulimit.Ulimit) flUlimits := opts.NewUlimitOpt(&ulimits) cmd.Var(flUlimits, []string{"-ulimit"}, "Ulimit options") cmd.Require(flag.Exact, 1) // For trusted pull on "FROM <image>" instruction. addTrustedFlags(cmd, true) cmd.ParseFlags(args, true) var ( context io.ReadCloser isRemote bool err error ) _, err = exec.LookPath("git") hasGit := err == nil specifiedContext := cmd.Arg(0) var ( contextDir string tempDir string relDockerfile string ) switch { case specifiedContext == "-": tempDir, relDockerfile, err = getContextFromReader(cli.in, *dockerfileName) case urlutil.IsGitURL(specifiedContext) && hasGit: tempDir, relDockerfile, err = getContextFromGitURL(specifiedContext, *dockerfileName) case urlutil.IsURL(specifiedContext): tempDir, relDockerfile, err = getContextFromURL(cli.out, specifiedContext, *dockerfileName) default: contextDir, relDockerfile, err = getContextFromLocalDir(specifiedContext, *dockerfileName) } if err != nil { return fmt.Errorf("unable to prepare context: %s", err) } if tempDir != "" { defer os.RemoveAll(tempDir) contextDir = tempDir } // Resolve the FROM lines in the Dockerfile to trusted digest references // using Notary. On a successful build, we must tag the resolved digests // to the original name specified in the Dockerfile. newDockerfile, resolvedTags, err := rewriteDockerfileFrom(filepath.Join(contextDir, relDockerfile), cli.trustedReference) if err != nil { return fmt.Errorf("unable to process Dockerfile: %v", err) } defer newDockerfile.Close() // And canonicalize dockerfile name to a platform-independent one relDockerfile, err = archive.CanonicalTarNameForPath(relDockerfile) if err != nil { return fmt.Errorf("cannot canonicalize dockerfile path %s: %v", relDockerfile, err) } f, err := os.Open(filepath.Join(contextDir, ".dockerignore")) if err != nil && !os.IsNotExist(err) { return err } var excludes []string if err == nil { excludes, err = utils.ReadDockerIgnore(f) if err != nil { return err } } if err := utils.ValidateContextDirectory(contextDir, excludes); err != nil { return fmt.Errorf("Error checking context: '%s'.", err) } // If .dockerignore mentions .dockerignore or the Dockerfile // then make sure we send both files over to the daemon // because Dockerfile is, obviously, needed no matter what, and // .dockerignore is needed to know if either one needs to be // removed. The deamon will remove them for us, if needed, after it // parses the Dockerfile. Ignore errors here, as they will have been // caught by ValidateContextDirectory above. var includes = []string{"."} keepThem1, _ := fileutils.Matches(".dockerignore", excludes) keepThem2, _ := fileutils.Matches(relDockerfile, excludes) if keepThem1 || keepThem2 { includes = append(includes, ".dockerignore", relDockerfile) } context, err = archive.TarWithOptions(contextDir, &archive.TarOptions{ Compression: archive.Uncompressed, ExcludePatterns: excludes, IncludeFiles: includes, }) if err != nil { return err } // Wrap the tar archive to replace the Dockerfile entry with the rewritten // Dockerfile which uses trusted pulls. context = replaceDockerfileTarWrapper(context, newDockerfile, relDockerfile) // Setup an upload progress bar // FIXME: ProgressReader shouldn't be this annoying to use sf := streamformatter.NewStreamFormatter() var body io.Reader = progressreader.New(progressreader.Config{ In: context, Out: cli.out, Formatter: sf, NewLines: true, ID: "", Action: "Sending build context to Docker daemon", }) var memory int64 if *flMemoryString != "" { parsedMemory, err := units.RAMInBytes(*flMemoryString) if err != nil { return err } memory = parsedMemory } var memorySwap int64 if *flMemorySwap != "" { if *flMemorySwap == "-1" { memorySwap = -1 } else { parsedMemorySwap, err := units.RAMInBytes(*flMemorySwap) if err != nil { return err } memorySwap = parsedMemorySwap } } // Send the build context v := url.Values{ "t": flTags.GetAll(), } if *suppressOutput { v.Set("q", "1") } if isRemote { v.Set("remote", cmd.Arg(0)) } if *noCache { v.Set("nocache", "1") } if *rm { v.Set("rm", "1") } else { v.Set("rm", "0") } if *forceRm { v.Set("forcerm", "1") } if *pull { v.Set("pull", "1") } v.Set("cpusetcpus", *flCPUSetCpus) v.Set("cpusetmems", *flCPUSetMems) v.Set("cpushares", strconv.FormatInt(*flCPUShares, 10)) v.Set("cpuquota", strconv.FormatInt(*flCPUQuota, 10)) v.Set("cpuperiod", strconv.FormatInt(*flCPUPeriod, 10)) v.Set("memory", strconv.FormatInt(memory, 10)) v.Set("memswap", strconv.FormatInt(memorySwap, 10)) v.Set("cgroupparent", *flCgroupParent) v.Set("dockerfile", relDockerfile) ulimitsVar := flUlimits.GetList() ulimitsJSON, err := json.Marshal(ulimitsVar) if err != nil { return err } v.Set("ulimits", string(ulimitsJSON)) // collect all the build-time environment variables for the container buildArgs := runconfig.ConvertKVStringsToMap(flBuildArg.GetAll()) buildArgsJSON, err := json.Marshal(buildArgs) if err != nil { return err } v.Set("buildargs", string(buildArgsJSON)) headers := http.Header(make(map[string][]string)) buf, err := json.Marshal(cli.configFile.AuthConfigs) if err != nil { return err } headers.Add("X-Registry-Config", base64.URLEncoding.EncodeToString(buf)) headers.Set("Content-Type", "application/tar") sopts := &streamOpts{ rawTerminal: true, in: body, out: cli.out, headers: headers, } serverResp, err := cli.stream("POST", fmt.Sprintf("/build?%s", v.Encode()), sopts) // Windows: show error message about modified file permissions. if runtime.GOOS == "windows" { h, err := httputils.ParseServerHeader(serverResp.header.Get("Server")) if err == nil { if h.OS != "windows" { 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.`) } } } if jerr, ok := err.(*jsonmessage.JSONError); ok { // If no error code is set, default to 1 if jerr.Code == 0 { jerr.Code = 1 } return Cli.StatusError{Status: jerr.Message, StatusCode: jerr.Code} } if err != nil { return err } // Since the build was successful, now we must tag any of the resolved // images from the above Dockerfile rewrite. for _, resolved := range resolvedTags { if err := cli.tagTrusted(resolved.repoInfo, resolved.digestRef, resolved.tagRef); err != nil { return err } } return nil }
func checkEnvIgnored(m *manifest.Manifest) error { //TODO if denv := filepath.Join(filepath.Dir(os.Args[0]), ".env"); exists(denv) { title := "<file>.env</file> in <file>.gitignore</file> and <file>.dockerignore</file>" startCheck(title) _, err := os.Stat(".dockerignore") if err != nil { diagnose(Diagnosis{ Title: title, Description: "<warning>It looks like you don't have a .dockerignore file</warning>", Kind: "warning", DocsLink: "https://docs.docker.com/engine/reference/builder/#/dockerignore-file", }) return nil } _, err = os.Stat(".gitignore") if err != nil { diagnose(Diagnosis{ Title: title, Description: "<warning>It looks like you don't have a .gitignore file</warning>", Kind: "warning", DocsLink: "https://git-scm.com/docs/gitignore", }) return nil } dockerIgnore, err := ioutil.ReadFile(filepath.Join(filepath.Dir(os.Args[0]), ".dockerignore")) if err != nil { return err } gitIgnore, err := ioutil.ReadFile(filepath.Join(filepath.Dir(os.Args[0]), ".gitignore")) if err != nil { return err } dockerIgnoreLines := strings.Split(string(dockerIgnore), "\n") gitIgnoreLines := strings.Split(string(gitIgnore), "\n") dockerIgnored, _ := fileutils.Matches(".env", dockerIgnoreLines) gitIgnored, _ := fileutils.Matches(".env", gitIgnoreLines) if !gitIgnored { diagnose(Diagnosis{ Title: title, Description: "<warning>It looks like you don't have your .env in your .gitignore file</warning>", Kind: "warning", DocsLink: "https://git-scm.com/docs/gitignore", }) } if !dockerIgnored { diagnose(Diagnosis{ Title: title, Description: "<warning>It looks like you don't have your .env in your .dockerignore file</warning>", Kind: "warning", DocsLink: "https://docs.docker.com/engine/reference/builder/#/dockerignore-file", }) } diagnose(Diagnosis{ Title: title, Kind: "success", }) } return nil }
// TarWithOptions creates an archive from the directory at `path`, only including files whose relative // paths are included in `options.Includes` (if non-nil) or not in `options.Excludes`. func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error) { pipeReader, pipeWriter := io.Pipe() compressWriter, err := CompressStream(pipeWriter, options.Compression) if err != nil { return nil, err } go func() { ta := &tarAppender{ TarWriter: tar.NewWriter(compressWriter), Buffer: pools.BufioWriter32KPool.Get(nil), SeenFiles: make(map[uint64]string), } // this buffer is needed for the duration of this piped stream defer pools.BufioWriter32KPool.Put(ta.Buffer) // In general we log errors here but ignore them because // during e.g. a diff operation the container can continue // mutating the filesystem and we can see transient errors // from this if options.Includes == nil { options.Includes = []string{"."} } var renamedRelFilePath string // For when tar.Options.Name is set for _, include := range options.Includes { filepath.Walk(filepath.Join(srcPath, include), func(filePath string, f os.FileInfo, err error) error { if err != nil { log.Debugf("Tar: Can't stat file %s to tar: %s", srcPath, err) return nil } relFilePath, err := filepath.Rel(srcPath, filePath) if err != nil { return nil } skip, err := fileutils.Matches(relFilePath, options.Excludes) if err != nil { log.Debugf("Error matching %s", relFilePath, err) return err } if skip { if f.IsDir() { return filepath.SkipDir } return nil } // Rename the base resource if options.Name != "" && filePath == srcPath+"/"+filepath.Base(relFilePath) { renamedRelFilePath = relFilePath } // Set this to make sure the items underneath also get renamed if options.Name != "" { relFilePath = strings.Replace(relFilePath, renamedRelFilePath, options.Name, 1) } if err := ta.addTarFile(filePath, relFilePath); err != nil { log.Debugf("Can't add file %s to tar: %s", srcPath, err) } return nil }) } // Make sure to check the error on Close. if err := ta.TarWriter.Close(); err != nil { log.Debugf("Can't close tar writer: %s", err) } if err := compressWriter.Close(); err != nil { log.Debugf("Can't close compress writer: %s", err) } if err := pipeWriter.Close(); err != nil { log.Debugf("Can't close pipe writer: %s", err) } }() return pipeReader, nil }
func createTarball(base string) ([]byte, error) { cwd, err := os.Getwd() if err != nil { return nil, err } sym, err := filepath.EvalSymlinks(base) if err != nil { return nil, err } err = os.Chdir(sym) if err != nil { return nil, err } var includes = []string{"."} var excludes []string dockerIgnorePath := path.Join(sym, ".dockerignore") dockerIgnore, err := os.Open(dockerIgnorePath) if err != nil { if !os.IsNotExist(err) { return nil, err } //There is no docker ignore excludes = make([]string, 0) } else { excludes, err = dockerignore.ReadAll(dockerIgnore) if err != nil { return nil, err } } // If .dockerignore mentions .dockerignore or the Dockerfile // then make sure we send both files over to the daemon // because Dockerfile is, obviously, needed no matter what, and // .dockerignore is needed to know if either one needs to be // removed. The deamon will remove them for us, if needed, after it // parses the Dockerfile. keepThem1, _ := fileutils.Matches(".dockerignore", excludes) keepThem2, _ := fileutils.Matches("Dockerfile", excludes) if keepThem1 || keepThem2 { includes = append(includes, ".dockerignore", "Dockerfile") } // if err := builder.ValidateContextDirectory(contextDirectory, excludes); err != nil { // return nil, fmt.Errorf("Error checking context is accessible: '%s'. Please check permissions and try again.", err) // } options := &archive.TarOptions{ Compression: archive.Gzip, ExcludePatterns: excludes, IncludeFiles: includes, } out, err := archive.TarWithOptions(sym, options) if err != nil { return nil, err } bytes, err := ioutil.ReadAll(out) if err != nil { return nil, err } err = os.Chdir(cwd) if err != nil { return nil, err } return bytes, nil }
func collectFileInfo(sourceDir string, options *ChangeOptions) (*FileInfo, error) { root := newRootFileInfo() patterns := make(map[string][]string) if options != nil && len(options.Includes) > 0 { patterns = fileutils.MapFilePaths(options.Includes) log.Infof("Map file path: %v", patterns) } log.Infof("%s Change Options: %v", sourceDir, options) err := filepath.Walk(sourceDir, func(path string, f os.FileInfo, err error) error { if err != nil { return err } // Rebase path relPath, err := filepath.Rel(sourceDir, path) if err != nil { return err } relPath = filepath.Join("/", relPath) if relPath == "/" { return nil } if options != nil { var ( skip = false keep = true ) skip, err = fileutils.Matches(relPath, options.Excludes) if err != nil { log.Debugf("Error matching %s", relPath, err) return err } if len(patterns) > 0 { keep, err = fileutils.RecursiveMatches(relPath, patterns) if err != nil { log.Debugf("Error recursive matching %s", relPath, err) return err } } if skip || !keep { if f.IsDir() { return filepath.SkipDir } return nil } } parent := root.LookUp(filepath.Dir(relPath)) if parent == nil { return fmt.Errorf("collectFileInfo: Unexpectedly no parent for %s", relPath) } info := &FileInfo{ name: filepath.Base(relPath), children: make(map[string]*FileInfo), parent: parent, } if err := syscall.Lstat(path, &info.stat); err != nil { return err } info.capability, _ = system.Lgetxattr(path, "security.capability") parent.children[info.name] = info return nil }) if err != nil { return nil, err } return root, nil }
// Reads a Dockerfile from the current context. It assumes that the // 'filename' is a relative path from the root of the context func (b *builder) readDockerfile() error { // If no -f was specified then look for 'Dockerfile'. If we can't find // that then look for 'dockerfile'. If neither are found then default // back to 'Dockerfile' and use that in the error message. if b.dockerfileName == "" { b.dockerfileName = api.DefaultDockerfileName tmpFN := filepath.Join(b.contextPath, api.DefaultDockerfileName) if _, err := os.Lstat(tmpFN); err != nil { tmpFN = filepath.Join(b.contextPath, strings.ToLower(api.DefaultDockerfileName)) if _, err := os.Lstat(tmpFN); err == nil { b.dockerfileName = strings.ToLower(api.DefaultDockerfileName) } } } origFile := b.dockerfileName filename, err := symlink.FollowSymlinkInScope(filepath.Join(b.contextPath, origFile), b.contextPath) if err != nil { return fmt.Errorf("The Dockerfile (%s) must be within the build context", origFile) } fi, err := os.Lstat(filename) if os.IsNotExist(err) { return fmt.Errorf("Cannot locate specified Dockerfile: %s", origFile) } if fi.Size() == 0 { return fmt.Errorf("The Dockerfile (%s) cannot be empty", origFile) } f, err := os.Open(filename) if err != nil { return err } b.dockerfile, err = parser.Parse(f) f.Close() if err != nil { return err } // After the Dockerfile has been parsed, we need to check the .dockerignore // file for either "Dockerfile" or ".dockerignore", and if either are // present then erase them from the build context. These files should never // have been sent from the client but we did send them to make sure that // we had the Dockerfile to actually parse, and then we also need the // .dockerignore file to know whether either file should be removed. // Note that this assumes the Dockerfile has been read into memory and // is now safe to be removed. excludes, _ := utils.ReadDockerIgnore(filepath.Join(b.contextPath, ".dockerignore")) if rm, _ := fileutils.Matches(".dockerignore", excludes); rm == true { os.Remove(filepath.Join(b.contextPath, ".dockerignore")) b.context.(tarsum.BuilderContext).Remove(".dockerignore") } if rm, _ := fileutils.Matches(b.dockerfileName, excludes); rm == true { os.Remove(filepath.Join(b.contextPath, b.dockerfileName)) b.context.(tarsum.BuilderContext).Remove(b.dockerfileName) } return nil }
// CreateTar create a build context tar for the specified project and service name. func CreateTar(p *project.Project, name string) (io.ReadCloser, error) { // This code was ripped off from docker/api/client/build.go serviceConfig := p.Configs[name] root := serviceConfig.Build dockerfileName := filepath.Join(root, serviceConfig.Dockerfile) absRoot, err := filepath.Abs(root) if err != nil { return nil, err } filename := dockerfileName if dockerfileName == "" { // No -f/--file was specified so use the default dockerfileName = DefaultDockerfileName filename = filepath.Join(absRoot, dockerfileName) // Just to be nice ;-) look for 'dockerfile' too but only // use it if we found it, otherwise ignore this check if _, err = os.Lstat(filename); os.IsNotExist(err) { tmpFN := path.Join(absRoot, strings.ToLower(dockerfileName)) if _, err = os.Lstat(tmpFN); err == nil { dockerfileName = strings.ToLower(dockerfileName) filename = tmpFN } } } origDockerfile := dockerfileName // used for error msg if filename, err = filepath.Abs(filename); err != nil { return nil, err } // Now reset the dockerfileName to be relative to the build context dockerfileName, err = filepath.Rel(absRoot, filename) if err != nil { return nil, err } // And canonicalize dockerfile name to a platform-independent one dockerfileName, err = archive.CanonicalTarNameForPath(dockerfileName) if err != nil { return nil, fmt.Errorf("Cannot canonicalize dockerfile path %s: %v", dockerfileName, err) } if _, err = os.Lstat(filename); os.IsNotExist(err) { return nil, fmt.Errorf("Cannot locate Dockerfile: %s", origDockerfile) } var includes = []string{"."} var excludes []string dockerIgnorePath := path.Join(root, ".dockerignore") dockerIgnore, err := os.Open(dockerIgnorePath) if err != nil { if !os.IsNotExist(err) { return nil, err } logrus.Warnf("Error while reading .dockerignore (%s) : %s", dockerIgnorePath, err.Error()) excludes = make([]string, 0) } else { excludes, err = utils.ReadDockerIgnore(dockerIgnore) if err != nil { return nil, err } } // If .dockerignore mentions .dockerignore or the Dockerfile // then make sure we send both files over to the daemon // because Dockerfile is, obviously, needed no matter what, and // .dockerignore is needed to know if either one needs to be // removed. The deamon will remove them for us, if needed, after it // parses the Dockerfile. keepThem1, _ := fileutils.Matches(".dockerignore", excludes) keepThem2, _ := fileutils.Matches(dockerfileName, excludes) if keepThem1 || keepThem2 { includes = append(includes, ".dockerignore", dockerfileName) } if err := utils.ValidateContextDirectory(root, excludes); err != nil { return nil, fmt.Errorf("Error checking context is accessible: '%s'. Please check permissions and try again.", err) } options := &archive.TarOptions{ Compression: archive.Uncompressed, ExcludePatterns: excludes, IncludeFiles: includes, } return archive.TarWithOptions(root, options) }
// TarWithOptions creates an archive from the directory at `path`, only including files whose relative // paths are included in `options.Includes` (if non-nil) or not in `options.Excludes`. func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error) { pipeReader, pipeWriter := io.Pipe() compressWriter, err := CompressStream(pipeWriter, options.Compression) if err != nil { return nil, err } tw := tar.NewWriter(compressWriter) go func() { // In general we log errors here but ignore them because // during e.g. a diff operation the container can continue // mutating the filesystem and we can see transient errors // from this if options.Includes == nil { options.Includes = []string{"."} } twBuf := pools.BufioWriter32KPool.Get(nil) defer pools.BufioWriter32KPool.Put(twBuf) for _, include := range options.Includes { filepath.Walk(filepath.Join(srcPath, include), func(filePath string, f os.FileInfo, err error) error { if err != nil { log.Debugf("Tar: Can't stat file %s to tar: %s", srcPath, err) return nil } relFilePath, err := filepath.Rel(srcPath, filePath) if err != nil { return nil } skip, err := fileutils.Matches(relFilePath, options.Excludes) if err != nil { log.Debugf("Error matching %s", relFilePath, err) return err } if skip { if f.IsDir() { return filepath.SkipDir } return nil } if err := addTarFile(filePath, relFilePath, tw, twBuf); err != nil { log.Debugf("Can't add file %s to tar: %s", srcPath, err) } return nil }) } // Make sure to check the error on Close. if err := tw.Close(); err != nil { log.Debugf("Can't close tar writer: %s", err) } if err := compressWriter.Close(); err != nil { log.Debugf("Can't close compress writer: %s", err) } if err := pipeWriter.Close(); err != nil { log.Debugf("Can't close pipe writer: %s", err) } }() return pipeReader, nil }
// hyper build [OPTIONS] PATH func (cli *HyperClient) HyperCmdBuild(args ...string) error { var opts struct { ImageName string `long:"tag" short:"t" default:"" value-name:"\"\"" default-mask:"-" description:"Repository name (and optionally a tag) to be applied to the resulting image in case of success"` DockerfileName string `long:"file" short:"f" default:"" value-name:"\"\"" default-mask:"-" description:"Customized docker file"` } var parser = gflag.NewParser(&opts, gflag.Default) parser.Usage = "build [OPTIONS] PATH\n\nBuild a new image from the source code at PATH" args, err := parser.ParseArgs(args) if err != nil { if !strings.Contains(err.Error(), "Usage") { return err } else { return nil } } if len(args) == 0 { return fmt.Errorf("%s: \"build\" requires a minimum of 1 argument, See 'hyper build --help'.", os.Args[0]) } var ( filename = "" context archive.Archive name = "" ) root := args[0] if _, err := os.Stat(root); err != nil { return err } absRoot, err := filepath.Abs(root) if err != nil { return err } filename = opts.DockerfileName // path to Dockerfile if opts.DockerfileName == "" { // No -f/--file was specified so use the default opts.DockerfileName = api.DefaultDockerfileName filename = filepath.Join(absRoot, opts.DockerfileName) // Just to be nice ;-) look for 'dockerfile' too but only // use it if we found it, otherwise ignore this check if _, err = os.Lstat(filename); os.IsNotExist(err) { tmpFN := path.Join(absRoot, strings.ToLower(opts.DockerfileName)) if _, err = os.Lstat(tmpFN); err == nil { opts.DockerfileName = strings.ToLower(opts.DockerfileName) filename = tmpFN } } } origDockerfile := opts.DockerfileName // used for error msg if filename, err = filepath.Abs(filename); err != nil { return err } // Verify that 'filename' is within the build context filename, err = symlink.FollowSymlinkInScope(filename, absRoot) if err != nil { return fmt.Errorf("The Dockerfile (%s) must be within the build context (%s)", origDockerfile, root) } // Now reset the dockerfileName to be relative to the build context opts.DockerfileName, err = filepath.Rel(absRoot, filename) if err != nil { return err } // And canonicalize dockerfile name to a platform-independent one opts.DockerfileName, err = archive.CanonicalTarNameForPath(opts.DockerfileName) if err != nil { return fmt.Errorf("Cannot canonicalize dockerfile path %s: %v", opts.DockerfileName, err) } if _, err = os.Lstat(filename); os.IsNotExist(err) { return fmt.Errorf("Cannot locate Dockerfile: %s", origDockerfile) } var includes = []string{"."} f, err := os.Open(filepath.Join(root, ".dockerignore")) if err != nil && !os.IsNotExist(err) { return err } defer f.Close() var excludes []string if err == nil { excludes, err = utils.ReadDockerIgnore(f) if err != nil { return err } } if err := utils.ValidateContextDirectory(root, excludes); err != nil { return fmt.Errorf("Error checking context: '%s'.", err) } // If .dockerignore mentions .dockerignore or the Dockerfile // then make sure we send both files over to the daemon // because Dockerfile is, obviously, needed no matter what, and // .dockerignore is needed to know if either one needs to be // removed. The deamon will remove them for us, if needed, after it // parses the Dockerfile. keepThem1, _ := fileutils.Matches(".dockerignore", excludes) keepThem2, _ := fileutils.Matches(opts.DockerfileName, excludes) if keepThem1 || keepThem2 { includes = append(includes, ".dockerignore", opts.DockerfileName) } if err := utils.ValidateContextDirectory(root, excludes); err != nil { return fmt.Errorf("Error checking context: '%s'.", err) } options := &archive.TarOptions{ Compression: archive.Uncompressed, ExcludePatterns: excludes, IncludeFiles: includes, } context, err = archive.TarWithOptions(root, options) if err != nil { return err } var body io.Reader // Setup an upload progress bar // FIXME: ProgressReader shouldn't be this annoying to use if context != nil { sf := streamformatter.NewStreamFormatter() body = progressreader.New(progressreader.Config{ In: context, Out: os.Stdout, Formatter: sf, NewLines: true, ID: "", Action: "Sending build context to Docker daemon", }) } if opts.ImageName == "" { // set a image name name = rand.RandStr(10, "alphanum") } else { name = opts.ImageName repository, tag := parsers.ParseRepositoryTag(name) if err := registry.ValidateRepositoryName(repository); err != nil { return err } if len(tag) > 0 { if err := tags.ValidateTagName(tag); err != nil { return err } } } v := url.Values{} v.Set("name", name) headers := http.Header(make(map[string][]string)) if context != nil { headers.Set("Content-Type", "application/tar") } err = cli.stream("POST", "/image/build?"+v.Encode(), body, cli.out, headers) if err != nil { return err } return nil }
// CmdBuild builds a new image from the source code at a given path. // // If '-' is provided instead of a path or URL, Docker will build an image from either a Dockerfile or tar archive read from STDIN. // // Usage: docker build [OPTIONS] PATH | URL | - func (cli *DockerCli) CmdBuild(args ...string) error { cmd := Cli.Subcmd("build", []string{"PATH | URL | -"}, Cli.DockerCommands["build"].Description, true) flTags := opts.NewListOpts(validateTag) cmd.Var(&flTags, []string{"t", "-tag"}, "Name and optionally a tag in the 'name:tag' format") suppressOutput := cmd.Bool([]string{"q", "-quiet"}, false, "Suppress the build output and print image ID on success") noCache := cmd.Bool([]string{"-no-cache"}, false, "Do not use cache when building the image") rm := cmd.Bool([]string{"-rm"}, true, "Remove intermediate containers after a successful build") forceRm := cmd.Bool([]string{"-force-rm"}, false, "Always remove intermediate containers") pull := cmd.Bool([]string{"-pull"}, false, "Always attempt to pull a newer version of the image") dockerfileName := cmd.String([]string{"f", "-file"}, "", "Name of the Dockerfile (Default is 'PATH/Dockerfile')") flMemoryString := cmd.String([]string{"m", "-memory"}, "", "Memory limit") flMemorySwap := cmd.String([]string{"-memory-swap"}, "", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap") flShmSize := cmd.String([]string{"-shm-size"}, "", "Size of /dev/shm, default value is 64MB") flCPUShares := cmd.Int64([]string{"#c", "-cpu-shares"}, 0, "CPU shares (relative weight)") flCPUPeriod := cmd.Int64([]string{"-cpu-period"}, 0, "Limit the CPU CFS (Completely Fair Scheduler) period") flCPUQuota := cmd.Int64([]string{"-cpu-quota"}, 0, "Limit the CPU CFS (Completely Fair Scheduler) quota") flCPUSetCpus := cmd.String([]string{"-cpuset-cpus"}, "", "CPUs in which to allow execution (0-3, 0,1)") flCPUSetMems := cmd.String([]string{"-cpuset-mems"}, "", "MEMs in which to allow execution (0-3, 0,1)") flCgroupParent := cmd.String([]string{"-cgroup-parent"}, "", "Optional parent cgroup for the container") flBuildArg := opts.NewListOpts(runconfigopts.ValidateEnv) cmd.Var(&flBuildArg, []string{"-build-arg"}, "Set build-time variables") isolation := cmd.String([]string{"-isolation"}, "", "Container isolation level") ulimits := make(map[string]*units.Ulimit) flUlimits := runconfigopts.NewUlimitOpt(&ulimits) cmd.Var(flUlimits, []string{"-ulimit"}, "Ulimit options") cmd.Require(flag.Exact, 1) // For trusted pull on "FROM <image>" instruction. addTrustedFlags(cmd, true) cmd.ParseFlags(args, true) var ( context io.ReadCloser isRemote bool err error ) _, err = exec.LookPath("git") hasGit := err == nil specifiedContext := cmd.Arg(0) var ( contextDir string tempDir string relDockerfile string progBuff io.Writer buildBuff io.Writer ) progBuff = cli.out buildBuff = cli.out if *suppressOutput { progBuff = bytes.NewBuffer(nil) buildBuff = bytes.NewBuffer(nil) } switch { case specifiedContext == "-": context, relDockerfile, err = getContextFromReader(cli.in, *dockerfileName) case urlutil.IsGitURL(specifiedContext) && hasGit: tempDir, relDockerfile, err = getContextFromGitURL(specifiedContext, *dockerfileName) case urlutil.IsURL(specifiedContext): context, relDockerfile, err = getContextFromURL(progBuff, specifiedContext, *dockerfileName) default: contextDir, relDockerfile, err = getContextFromLocalDir(specifiedContext, *dockerfileName) } if err != nil { if *suppressOutput && urlutil.IsURL(specifiedContext) { fmt.Fprintln(cli.err, progBuff) } return fmt.Errorf("unable to prepare context: %s", err) } if tempDir != "" { defer os.RemoveAll(tempDir) contextDir = tempDir } if context == nil { // And canonicalize dockerfile name to a platform-independent one relDockerfile, err = archive.CanonicalTarNameForPath(relDockerfile) if err != nil { return fmt.Errorf("cannot canonicalize dockerfile path %s: %v", relDockerfile, err) } f, err := os.Open(filepath.Join(contextDir, ".dockerignore")) if err != nil && !os.IsNotExist(err) { return err } var excludes []string if err == nil { excludes, err = dockerignore.ReadAll(f) if err != nil { return err } } if err := validateContextDirectory(contextDir, excludes); err != nil { return fmt.Errorf("Error checking context: '%s'.", err) } // If .dockerignore mentions .dockerignore or the Dockerfile // then make sure we send both files over to the daemon // because Dockerfile is, obviously, needed no matter what, and // .dockerignore is needed to know if either one needs to be // removed. The daemon will remove them for us, if needed, after it // parses the Dockerfile. Ignore errors here, as they will have been // caught by validateContextDirectory above. var includes = []string{"."} keepThem1, _ := fileutils.Matches(".dockerignore", excludes) keepThem2, _ := fileutils.Matches(relDockerfile, excludes) if keepThem1 || keepThem2 { includes = append(includes, ".dockerignore", relDockerfile) } context, err = archive.TarWithOptions(contextDir, &archive.TarOptions{ Compression: archive.Uncompressed, ExcludePatterns: excludes, IncludeFiles: includes, }) if err != nil { return err } } var resolvedTags []*resolvedTag if isTrusted() { // Wrap the tar archive to replace the Dockerfile entry with the rewritten // Dockerfile which uses trusted pulls. context = replaceDockerfileTarWrapper(context, relDockerfile, cli.trustedReference, &resolvedTags) } // Setup an upload progress bar progressOutput := streamformatter.NewStreamFormatter().NewProgressOutput(progBuff, true) var body io.Reader = progress.NewProgressReader(context, progressOutput, 0, "", "Sending build context to Docker daemon") var memory int64 if *flMemoryString != "" { parsedMemory, err := units.RAMInBytes(*flMemoryString) if err != nil { return err } memory = parsedMemory } var memorySwap int64 if *flMemorySwap != "" { if *flMemorySwap == "-1" { memorySwap = -1 } else { parsedMemorySwap, err := units.RAMInBytes(*flMemorySwap) if err != nil { return err } memorySwap = parsedMemorySwap } } var shmSize int64 if *flShmSize != "" { shmSize, err = units.RAMInBytes(*flShmSize) if err != nil { return err } } var remoteContext string if isRemote { remoteContext = cmd.Arg(0) } options := types.ImageBuildOptions{ Context: body, Memory: memory, MemorySwap: memorySwap, Tags: flTags.GetAll(), SuppressOutput: *suppressOutput, RemoteContext: remoteContext, NoCache: *noCache, Remove: *rm, ForceRemove: *forceRm, PullParent: *pull, IsolationLevel: container.IsolationLevel(*isolation), CPUSetCPUs: *flCPUSetCpus, CPUSetMems: *flCPUSetMems, CPUShares: *flCPUShares, CPUQuota: *flCPUQuota, CPUPeriod: *flCPUPeriod, CgroupParent: *flCgroupParent, Dockerfile: relDockerfile, ShmSize: shmSize, Ulimits: flUlimits.GetList(), BuildArgs: runconfigopts.ConvertKVStringsToMap(flBuildArg.GetAll()), AuthConfigs: cli.configFile.AuthConfigs, } response, err := cli.client.ImageBuild(options) if err != nil { return err } err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, cli.outFd, cli.isTerminalOut, nil) if err != nil { if jerr, ok := err.(*jsonmessage.JSONError); ok { // If no error code is set, default to 1 if jerr.Code == 0 { jerr.Code = 1 } if *suppressOutput { fmt.Fprintf(cli.err, "%s%s", progBuff, buildBuff) } return Cli.StatusError{Status: jerr.Message, StatusCode: jerr.Code} } } // Windows: show error message about modified file permissions if the // daemon isn't running Windows. if response.OSType != "windows" && runtime.GOOS == "windows" { 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.`) } // Everything worked so if -q was provided the output from the daemon // should be just the image ID and we'll print that to stdout. if *suppressOutput { fmt.Fprintf(cli.out, "%s", buildBuff) } if isTrusted() { // Since the build was successful, now we must tag any of the resolved // images from the above Dockerfile rewrite. for _, resolved := range resolvedTags { if err := cli.tagTrusted(resolved.digestRef, resolved.tagRef); err != nil { return err } } } return nil }
func runBuild(dockerCli *command.DockerCli, options buildOptions) error { var ( buildCtx io.ReadCloser err error ) specifiedContext := options.context var ( contextDir string tempDir string relDockerfile string progBuff io.Writer buildBuff io.Writer ) progBuff = dockerCli.Out() buildBuff = dockerCli.Out() if options.quiet { progBuff = bytes.NewBuffer(nil) buildBuff = bytes.NewBuffer(nil) } switch { case specifiedContext == "-": buildCtx, relDockerfile, err = builder.GetContextFromReader(dockerCli.In(), options.dockerfileName) case urlutil.IsGitURL(specifiedContext): tempDir, relDockerfile, err = builder.GetContextFromGitURL(specifiedContext, options.dockerfileName) case urlutil.IsURL(specifiedContext): buildCtx, relDockerfile, err = builder.GetContextFromURL(progBuff, specifiedContext, options.dockerfileName) default: contextDir, relDockerfile, err = builder.GetContextFromLocalDir(specifiedContext, options.dockerfileName) } if err != nil { if options.quiet && urlutil.IsURL(specifiedContext) { fmt.Fprintln(dockerCli.Err(), progBuff) } return fmt.Errorf("unable to prepare context: %s", err) } if tempDir != "" { defer os.RemoveAll(tempDir) contextDir = tempDir } if buildCtx == nil { // And canonicalize dockerfile name to a platform-independent one relDockerfile, err = archive.CanonicalTarNameForPath(relDockerfile) if err != nil { return fmt.Errorf("cannot canonicalize dockerfile path %s: %v", relDockerfile, err) } f, err := os.Open(filepath.Join(contextDir, ".dockerignore")) if err != nil && !os.IsNotExist(err) { return err } defer f.Close() var excludes []string if err == nil { excludes, err = dockerignore.ReadAll(f) if err != nil { return err } } if err := builder.ValidateContextDirectory(contextDir, excludes); err != nil { return fmt.Errorf("Error checking context: '%s'.", err) } // If .dockerignore mentions .dockerignore or the Dockerfile // then make sure we send both files over to the daemon // because Dockerfile is, obviously, needed no matter what, and // .dockerignore is needed to know if either one needs to be // removed. The daemon will remove them for us, if needed, after it // parses the Dockerfile. Ignore errors here, as they will have been // caught by validateContextDirectory above. var includes = []string{"."} keepThem1, _ := fileutils.Matches(".dockerignore", excludes) keepThem2, _ := fileutils.Matches(relDockerfile, excludes) if keepThem1 || keepThem2 { includes = append(includes, ".dockerignore", relDockerfile) } buildCtx, err = archive.TarWithOptions(contextDir, &archive.TarOptions{ Compression: archive.Uncompressed, ExcludePatterns: excludes, IncludeFiles: includes, }) if err != nil { return err } } ctx := context.Background() var resolvedTags []*resolvedTag if command.IsTrusted() { translator := func(ctx context.Context, ref reference.NamedTagged) (reference.Canonical, error) { return TrustedReference(ctx, dockerCli, ref) } // Wrap the tar archive to replace the Dockerfile entry with the rewritten // Dockerfile which uses trusted pulls. buildCtx = replaceDockerfileTarWrapper(ctx, buildCtx, relDockerfile, translator, &resolvedTags) } // Setup an upload progress bar progressOutput := streamformatter.NewStreamFormatter().NewProgressOutput(progBuff, true) if !dockerCli.Out().IsTerminal() { progressOutput = &lastProgressOutput{output: progressOutput} } var body io.Reader = progress.NewProgressReader(buildCtx, progressOutput, 0, "", "Sending build context to Docker daemon") var memory int64 if options.memory != "" { parsedMemory, err := units.RAMInBytes(options.memory) if err != nil { return err } memory = parsedMemory } var memorySwap int64 if options.memorySwap != "" { if options.memorySwap == "-1" { memorySwap = -1 } else { parsedMemorySwap, err := units.RAMInBytes(options.memorySwap) if err != nil { return err } memorySwap = parsedMemorySwap } } var shmSize int64 if options.shmSize != "" { shmSize, err = units.RAMInBytes(options.shmSize) if err != nil { return err } } authConfig, _ := dockerCli.CredentialsStore().GetAll() buildOptions := types.ImageBuildOptions{ Memory: memory, MemorySwap: memorySwap, Tags: options.tags.GetAll(), SuppressOutput: options.quiet, NoCache: options.noCache, Remove: options.rm, ForceRemove: options.forceRm, PullParent: options.pull, Isolation: container.Isolation(options.isolation), CPUSetCPUs: options.cpuSetCpus, CPUSetMems: options.cpuSetMems, CPUShares: options.cpuShares, CPUQuota: options.cpuQuota, CPUPeriod: options.cpuPeriod, CgroupParent: options.cgroupParent, Dockerfile: relDockerfile, ShmSize: shmSize, Ulimits: options.ulimits.GetList(), BuildArgs: runconfigopts.ConvertKVStringsToMap(options.buildArgs.GetAll()), AuthConfigs: authConfig, Labels: runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()), CacheFrom: options.cacheFrom, } response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions) if err != nil { if options.quiet { fmt.Fprintf(dockerCli.Err(), "%s", progBuff) } return err } defer response.Body.Close() err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, dockerCli.Out().FD(), dockerCli.Out().IsTerminal(), nil) if err != nil { if jerr, ok := err.(*jsonmessage.JSONError); ok { // If no error code is set, default to 1 if jerr.Code == 0 { jerr.Code = 1 } if options.quiet { fmt.Fprintf(dockerCli.Err(), "%s%s", progBuff, buildBuff) } return cli.StatusError{Status: jerr.Message, StatusCode: jerr.Code} } } // Windows: show error message about modified file permissions if the // daemon isn't running Windows. if response.OSType != "windows" && runtime.GOOS == "windows" && !options.quiet { 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.`) } // Everything worked so if -q was provided the output from the daemon // should be just the image ID and we'll print that to stdout. if options.quiet { fmt.Fprintf(dockerCli.Out(), "%s", buildBuff) } if command.IsTrusted() { // Since the build was successful, now we must tag any of the resolved // images from the above Dockerfile rewrite. for _, resolved := range resolvedTags { if err := TagTrusted(ctx, dockerCli, resolved.digestRef, resolved.tagRef); err != nil { return err } } } return nil }