package daemonbuilder import ( "fmt" "io" "io/ioutil" "os" "path/filepath" "strings" "github.com/Sirupsen/logrus" "github.com/docker/docker/api" "github.com/docker/docker/builder" "github.com/docker/docker/daemon" "github.com/docker/docker/image" "github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/httputils" "github.com/docker/docker/pkg/idtools" "github.com/docker/docker/pkg/ioutils" "github.com/docker/docker/pkg/urlutil" "github.com/docker/docker/reference" "github.com/docker/docker/registry" "github.com/docker/engine-api/types" "github.com/docker/engine-api/types/container" ) // Docker implements builder.Backend for the docker Daemon object. type Docker struct { *daemon.Daemon OutOld io.Writer AuthConfigs map[string]types.AuthConfig Archiver *archive.Archiver } // ensure Docker implements builder.Backend var _ builder.Backend = Docker{} // Pull tells Docker to pull image referenced by `name`. func (d Docker) Pull(name string) (builder.Image, error) { ref, err := reference.ParseNamed(name) if err != nil { return nil, err } ref = reference.WithDefaultTag(ref) pullRegistryAuth := &types.AuthConfig{} if len(d.AuthConfigs) > 0 { // The request came with a full auth config file, we prefer to use that repoInfo, err := d.Daemon.RegistryService.ResolveRepository(ref) if err != nil { return nil, err } resolvedConfig := registry.ResolveAuthConfig( d.AuthConfigs, repoInfo.Index, ) pullRegistryAuth = &resolvedConfig } if err := d.Daemon.PullImage(ref, nil, pullRegistryAuth, ioutils.NopWriteCloser(d.OutOld)); err != nil { return nil, err } return d.GetImage(name) } // GetImage looks up a Docker image referenced by `name`. func (d Docker) GetImage(name string) (builder.Image, error) { img, err := d.Daemon.GetImage(name) if err != nil { return nil, err } return imgWrap{img}, nil } // ContainerUpdateCmd updates Path and Args for the container with ID cID. func (d Docker) ContainerUpdateCmd(cID string, cmd []string) error { c, err := d.Daemon.GetContainer(cID) if err != nil { return err } c.Path = cmd[0] c.Args = cmd[1:] return nil } // ContainerAttach attaches streams to the container cID. If stream is true, it streams the output. func (d Docker) ContainerAttach(cID string, stdin io.ReadCloser, stdout, stderr io.Writer, stream bool) error { return d.Daemon.ContainerWsAttachWithLogs(cID, &daemon.ContainerWsAttachWithLogsConfig{ InStream: stdin, OutStream: stdout, ErrStream: stderr, Stream: stream, }) } // BuilderCopy copies/extracts a source FileInfo to a destination path inside a container // specified by a container object. // TODO: make sure callers don't unnecessarily convert destPath with filepath.FromSlash (Copy does it already). // BuilderCopy should take in abstract paths (with slashes) and the implementation should convert it to OS-specific paths. func (d Docker) BuilderCopy(cID string, destPath string, src builder.FileInfo, decompress bool) error { srcPath := src.Path() destExists := true destDir := false rootUID, rootGID := d.Daemon.GetRemappedUIDGID() // Work in daemon-local OS specific file paths destPath = filepath.FromSlash(destPath) c, err := d.Daemon.GetContainer(cID) if err != nil { return err } err = d.Daemon.Mount(c) if err != nil { return err } defer d.Daemon.Unmount(c) dest, err := c.GetResourcePath(destPath) if err != nil { return err } // Preserve the trailing slash // TODO: why are we appending another path separator if there was already one? if strings.HasSuffix(destPath, string(os.PathSeparator)) || destPath == "." { destDir = true dest += string(os.PathSeparator) } destPath = dest destStat, err := os.Stat(destPath) if err != nil { if !os.IsNotExist(err) { logrus.Errorf("Error performing os.Stat on %s. %s", destPath, err) return err } destExists = false } if src.IsDir() { // copy as directory if err := d.Archiver.CopyWithTar(srcPath, destPath); err != nil { return err } return fixPermissions(srcPath, destPath, rootUID, rootGID, destExists) } if decompress && archive.IsArchivePath(srcPath) { // Only try to untar if it is a file and that we've been told to decompress (when ADD-ing a remote file) // First try to unpack the source as an archive // to support the untar feature we need to clean up the path a little bit // because tar is very forgiving. First we need to strip off the archive's // filename from the path but this is only added if it does not end in slash tarDest := destPath if strings.HasSuffix(tarDest, string(os.PathSeparator)) { tarDest = filepath.Dir(destPath) } // try to successfully untar the orig err := d.Archiver.UntarPath(srcPath, tarDest) if err != nil { logrus.Errorf("Couldn't untar to %s: %v", tarDest, err) } return err } // only needed for fixPermissions, but might as well put it before CopyFileWithTar if destDir || (destExists && destStat.IsDir()) { destPath = filepath.Join(destPath, src.Name()) } if err := idtools.MkdirAllNewAs(filepath.Dir(destPath), 0755, rootUID, rootGID); err != nil { return err } if err := d.Archiver.CopyFileWithTar(srcPath, destPath); err != nil { return err } return fixPermissions(srcPath, destPath, rootUID, rootGID, destExists) } // GetCachedImage returns a reference to a cached image whose parent equals `parent` // and runconfig equals `cfg`. A cache miss is expected to return an empty ID and a nil error. func (d Docker) GetCachedImage(imgID string, cfg *container.Config) (string, error) { cache, err := d.Daemon.ImageGetCached(image.ID(imgID), cfg) if cache == nil || err != nil { return "", err } return cache.ID().String(), nil } // Following is specific to builder contexts // DetectContextFromRemoteURL returns a context and in certain cases the name of the dockerfile to be used // irrespective of user input. // progressReader is only used if remoteURL is actually a URL (not empty, and not a Git endpoint). func DetectContextFromRemoteURL(r io.ReadCloser, remoteURL string, createProgressReader func(in io.ReadCloser) io.ReadCloser) (context builder.ModifiableContext, dockerfileName string, err error) { switch { case remoteURL == "": context, err = builder.MakeTarSumContext(r) case urlutil.IsGitURL(remoteURL): context, err = builder.MakeGitContext(remoteURL) case urlutil.IsURL(remoteURL): context, err = builder.MakeRemoteContext(remoteURL, map[string]func(io.ReadCloser) (io.ReadCloser, error){ httputils.MimeTypes.TextPlain: func(rc io.ReadCloser) (io.ReadCloser, error) { dockerfile, err := ioutil.ReadAll(rc) if err != nil { return nil, err } // dockerfileName is set to signal that the remote was interpreted as a single Dockerfile, in which case the caller // should use dockerfileName as the new name for the Dockerfile, irrespective of any other user input. dockerfileName = api.DefaultDockerfileName // TODO: return a context without tarsum return archive.Generate(dockerfileName, string(dockerfile)) }, // fallback handler (tar context) "": func(rc io.ReadCloser) (io.ReadCloser, error) { return createProgressReader(rc), nil }, }) default: err = fmt.Errorf("remoteURL (%s) could not be recognized as URL", remoteURL) } return }