From 8ea2bd77d3c9eb669accd28d8a4305518b164a85 Mon Sep 17 00:00:00 2001 From: Josh Hawn Date: Tue, 21 Jul 2015 14:47:44 -0700 Subject: [PATCH] Add notary integration to `docker build` The Dockerfile is rewritten with images references on FROM instructions resolved to trusted digests. The rewritten Dockerfile is swapped with the original one during context upload. Docker-DCO-1.1-Signed-off-by: Josh Hawn (github: jlhawn) Upstream-commit: 578b1521df85eae8a6205118131751c631323ba5 Component: engine --- components/engine/api/client/build.go | 149 +++++++++++++++++++++++++- 1 file changed, 147 insertions(+), 2 deletions(-) diff --git a/components/engine/api/client/build.go b/components/engine/api/client/build.go index c9992be519..6dac4a3f39 100644 --- a/components/engine/api/client/build.go +++ b/components/engine/api/client/build.go @@ -1,6 +1,7 @@ package client import ( + "archive/tar" "bufio" "encoding/base64" "encoding/json" @@ -13,6 +14,7 @@ import ( "os/exec" "path" "path/filepath" + "regexp" "runtime" "strconv" "strings" @@ -112,6 +114,14 @@ func (cli *DockerCli) CmdBuild(args ...string) error { contextDir = tempDir } + // Resolve the FROM lines in the Dockerfile to trusted digest references + // using Notary. + newDockerfile, 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 { @@ -142,14 +152,19 @@ func (cli *DockerCli) CmdBuild(args ...string) error { includes = append(includes, ".dockerignore", relDockerfile) } - if context, err = archive.TarWithOptions(contextDir, &archive.TarOptions{ + context, err = archive.TarWithOptions(contextDir, &archive.TarOptions{ Compression: archive.Uncompressed, ExcludePatterns: excludes, IncludeFiles: includes, - }); err != nil { + }) + 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() @@ -439,3 +454,133 @@ func getContextFromLocalDir(localDir, dockerfileName string) (absContextDir, rel return getDockerfileRelPath(localDir, dockerfileName) } + +var dockerfileFromLinePattern = regexp.MustCompile(`(?i)^[\s]*FROM[ \f\r\t\v]+(?P[^ \f\r\t\v\n#]+)`) + +type trustedDockerfile struct { + *os.File + size int64 +} + +func (td *trustedDockerfile) Close() error { + td.File.Close() + return os.Remove(td.File.Name()) +} + +// rewriteDockerfileFrom rewrites the given Dockerfile by resolving images in +// "FROM " instructions to a digest reference. `translator` is a +// function that takes a repository name and tag reference and returns a +// trusted digest reference. +func rewriteDockerfileFrom(dockerfileName string, translator func(string, registry.Reference) (registry.Reference, error)) (newDockerfile *trustedDockerfile, err error) { + dockerfile, err := os.Open(dockerfileName) + if err != nil { + return nil, fmt.Errorf("unable to open Dockerfile: %v", err) + } + defer dockerfile.Close() + + scanner := bufio.NewScanner(dockerfile) + + // Make a tempfile to store the rewritten Dockerfile. + tempFile, err := ioutil.TempFile("", "trusted-dockerfile-") + if err != nil { + return nil, fmt.Errorf("unable to make temporary trusted Dockerfile: %v", err) + } + + trustedFile := &trustedDockerfile{ + File: tempFile, + } + + defer func() { + if err != nil { + // Close the tempfile if there was an error during Notary lookups. + // Otherwise the caller should close it. + trustedFile.Close() + } + }() + + // Scan the lines of the Dockerfile, looking for a "FROM" line. + for scanner.Scan() { + line := scanner.Text() + + matches := dockerfileFromLinePattern.FindStringSubmatch(line) + if matches != nil && matches[1] != "scratch" { + // Replace the line with a resolved "FROM repo@digest" + repo, tag := parsers.ParseRepositoryTag(matches[1]) + if tag == "" { + tag = tags.DEFAULTTAG + } + ref := registry.ParseReference(tag) + + if !ref.HasDigest() && isTrusted() { + trustedRef, err := translator(repo, ref) + if err != nil { + return nil, err + } + + line = dockerfileFromLinePattern.ReplaceAllLiteralString(line, fmt.Sprintf("FROM %s", trustedRef.ImageName(repo))) + } + } + + n, err := fmt.Fprintln(tempFile, line) + if err != nil { + return nil, err + } + + trustedFile.size += int64(n) + } + + tempFile.Seek(0, os.SEEK_SET) + + return trustedFile, scanner.Err() +} + +// replaceDockerfileTarWrapper wraps the given input tar archive stream and +// replaces the entry with the given Dockerfile name with the contents of the +// new Dockerfile. Returns a new tar archive stream with the replaced +// Dockerfile. +func replaceDockerfileTarWrapper(inputTarStream io.ReadCloser, newDockerfile *trustedDockerfile, dockerfileName string) io.ReadCloser { + pipeReader, pipeWriter := io.Pipe() + + go func() { + tarReader := tar.NewReader(inputTarStream) + tarWriter := tar.NewWriter(pipeWriter) + + defer inputTarStream.Close() + + for { + hdr, err := tarReader.Next() + if err == io.EOF { + // Signals end of archive. + tarWriter.Close() + pipeWriter.Close() + return + } + if err != nil { + pipeWriter.CloseWithError(err) + return + } + + var content io.Reader = tarReader + + if hdr.Name == dockerfileName { + // This entry is the Dockerfile. Since the tar archive was + // generated from a directory on the local filesystem, the + // Dockerfile will only appear once in the archive. + hdr.Size = newDockerfile.size + content = newDockerfile + } + + if err := tarWriter.WriteHeader(hdr); err != nil { + pipeWriter.CloseWithError(err) + return + } + + if _, err := io.Copy(tarWriter, content); err != nil { + pipeWriter.CloseWithError(err) + return + } + } + }() + + return pipeReader +}