Skip to content

Commit

Permalink
Merge pull request #35429 from dnephin/build-fails-on-long-label
Browse files Browse the repository at this point in the history
Fix dockerfile parser failing silently on long tokens
  • Loading branch information
thaJeztah committed Nov 15, 2017
2 parents a5c679b + a74cc83 commit 7c53e73
Show file tree
Hide file tree
Showing 6 changed files with 67 additions and 101 deletions.
11 changes: 10 additions & 1 deletion builder/dockerfile/parser/parser.go
Expand Up @@ -321,7 +321,7 @@ func Parse(rwc io.Reader) (*Result, error) {
Warnings: warnings,
EscapeToken: d.escapeToken,
OS: d.platformToken,
}, nil
}, handleScannerError(scanner.Err())
}

func trimComments(src []byte) []byte {
Expand Down Expand Up @@ -358,3 +358,12 @@ func processLine(d *Directive, token []byte, stripLeftWhitespace bool) ([]byte,
}
return trimComments(token), d.possibleParserDirective(string(token))
}

func handleScannerError(err error) error {
switch err {
case bufio.ErrTooLong:
return errors.Errorf("dockerfile line greater than max allowed size of %d", bufio.MaxScanTokenSize-1)
default:
return err
}
}
13 changes: 13 additions & 0 deletions builder/dockerfile/parser/parser_test.go
@@ -1,12 +1,14 @@
package parser

import (
"bufio"
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strings"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -159,3 +161,14 @@ RUN indented \
assert.Contains(t, warnings[1], "RUN another thing")
assert.Contains(t, warnings[2], "will become errors in a future release")
}

func TestParseReturnsScannerErrors(t *testing.T) {
label := strings.Repeat("a", bufio.MaxScanTokenSize)

dockerfile := strings.NewReader(fmt.Sprintf(`
FROM image
LABEL test=%s
`, label))
_, err := Parse(dockerfile)
assert.EqualError(t, err, "dockerfile line greater than max allowed size of 65535")
}
1 change: 0 additions & 1 deletion builder/remotecontext/archive.go
Expand Up @@ -79,7 +79,6 @@ func FromArchive(tarStream io.Reader) (builder.Source, error) {
}

tsc.sums = sum.GetSums()

return tsc, nil
}

Expand Down
33 changes: 15 additions & 18 deletions builder/remotecontext/detect.go
Expand Up @@ -97,26 +97,23 @@ func newGitRemote(gitURL string, dockerfilePath string) (builder.Source, *parser
}

func newURLRemote(url string, dockerfilePath string, progressReader func(in io.ReadCloser) io.ReadCloser) (builder.Source, *parser.Result, error) {
var dockerfile io.ReadCloser
dockerfileFoundErr := errors.New("found-dockerfile")
c, err := MakeRemoteContext(url, map[string]func(io.ReadCloser) (io.ReadCloser, error){
mimeTypes.TextPlain: func(rc io.ReadCloser) (io.ReadCloser, error) {
dockerfile = rc
return nil, dockerfileFoundErr
},
// fallback handler (tar context)
"": func(rc io.ReadCloser) (io.ReadCloser, error) {
return progressReader(rc), nil
},
})
switch {
case err == dockerfileFoundErr:
res, err := parser.Parse(dockerfile)
return nil, res, err
case err != nil:
contentType, content, err := downloadRemote(url)
if err != nil {
return nil, nil, err
}
return withDockerfileFromContext(c.(modifiableContext), dockerfilePath)
defer content.Close()

switch contentType {
case mimeTypes.TextPlain:
res, err := parser.Parse(progressReader(content))
return nil, res, err
default:
source, err := FromArchive(progressReader(content))
if err != nil {
return nil, nil, err
}
return withDockerfileFromContext(source.(modifiableContext), dockerfilePath)
}
}

func removeDockerfile(c modifiableContext, filesToRemove ...string) error {
Expand Down
61 changes: 17 additions & 44 deletions builder/remotecontext/remote.go
Expand Up @@ -10,7 +10,7 @@ import (
"net/url"
"regexp"

"github.com/docker/docker/builder"
"github.com/docker/docker/pkg/ioutils"
"github.com/pkg/errors"
)

Expand All @@ -22,50 +22,23 @@ const acceptableRemoteMIME = `(?:application/(?:(?:x\-)?tar|octet\-stream|((?:x\

var mimeRe = regexp.MustCompile(acceptableRemoteMIME)

// MakeRemoteContext downloads a context from remoteURL and returns it.
//
// If contentTypeHandlers is non-nil, then the Content-Type header is read along with a maximum of
// maxPreambleLength bytes from the body to help detecting the MIME type.
// Look at acceptableRemoteMIME for more details.
//
// If a match is found, then the body is sent to the contentType handler and a (potentially compressed) tar stream is expected
// to be returned. If no match is found, it is assumed the body is a tar stream (compressed or not).
// In either case, an (assumed) tar stream is passed to FromArchive whose result is returned.
func MakeRemoteContext(remoteURL string, contentTypeHandlers map[string]func(io.ReadCloser) (io.ReadCloser, error)) (builder.Source, error) {
f, err := GetWithStatusError(remoteURL)
// downloadRemote context from a url and returns it, along with the parsed content type
func downloadRemote(remoteURL string) (string, io.ReadCloser, error) {
response, err := GetWithStatusError(remoteURL)
if err != nil {
return nil, fmt.Errorf("error downloading remote context %s: %v", remoteURL, err)
return "", nil, fmt.Errorf("error downloading remote context %s: %v", remoteURL, err)
}
defer f.Body.Close()

var contextReader io.ReadCloser
if contentTypeHandlers != nil {
contentType := f.Header.Get("Content-Type")
clen := f.ContentLength

contentType, contextReader, err = inspectResponse(contentType, f.Body, clen)
if err != nil {
return nil, fmt.Errorf("error detecting content type for remote %s: %v", remoteURL, err)
}
defer contextReader.Close()

// This loop tries to find a content-type handler for the detected content-type.
// If it could not find one from the caller-supplied map, it tries the empty content-type `""`
// which is interpreted as a fallback handler (usually used for raw tar contexts).
for _, ct := range []string{contentType, ""} {
if fn, ok := contentTypeHandlers[ct]; ok {
defer contextReader.Close()
if contextReader, err = fn(contextReader); err != nil {
return nil, err
}
break
}
}
contentType, contextReader, err := inspectResponse(
response.Header.Get("Content-Type"),
response.Body,
response.ContentLength)
if err != nil {
response.Body.Close()
return "", nil, fmt.Errorf("error detecting content type for remote %s: %v", remoteURL, err)
}

// Pass through - this is a pre-packaged context, presumably
// with a Dockerfile with the right name inside it.
return FromArchive(contextReader)
return contentType, ioutils.NewReadCloserWrapper(contextReader, response.Body.Close), nil
}

// GetWithStatusError does an http.Get() and returns an error if the
Expand Down Expand Up @@ -110,7 +83,7 @@ func GetWithStatusError(address string) (resp *http.Response, err error) {
// - an io.Reader for the response body
// - an error value which will be non-nil either when something goes wrong while
// reading bytes from r or when the detected content-type is not acceptable.
func inspectResponse(ct string, r io.Reader, clen int64) (string, io.ReadCloser, error) {
func inspectResponse(ct string, r io.Reader, clen int64) (string, io.Reader, error) {
plen := clen
if plen <= 0 || plen > maxPreambleLength {
plen = maxPreambleLength
Expand All @@ -119,14 +92,14 @@ func inspectResponse(ct string, r io.Reader, clen int64) (string, io.ReadCloser,
preamble := make([]byte, plen)
rlen, err := r.Read(preamble)
if rlen == 0 {
return ct, ioutil.NopCloser(r), errors.New("empty response")
return ct, r, errors.New("empty response")
}
if err != nil && err != io.EOF {
return ct, ioutil.NopCloser(r), err
return ct, r, err
}

preambleR := bytes.NewReader(preamble[:rlen])
bodyReader := ioutil.NopCloser(io.MultiReader(preambleR, r))
bodyReader := io.MultiReader(preambleR, r)
// Some web servers will use application/octet-stream as the default
// content type for files without an extension (e.g. 'Dockerfile')
// so if we receive this value we better check for text content
Expand Down
49 changes: 12 additions & 37 deletions builder/remotecontext/remote_test.go
Expand Up @@ -11,7 +11,7 @@ import (

"github.com/docker/docker/builder"
"github.com/docker/docker/internal/testutil"
"github.com/docker/docker/pkg/archive"
"github.com/gotestyourself/gotestyourself/fs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -174,11 +174,10 @@ func TestUnknownContentLength(t *testing.T) {
}
}

func TestMakeRemoteContext(t *testing.T) {
contextDir, cleanup := createTestTempDir(t, "", "builder-tarsum-test")
defer cleanup()

createTestTempFile(t, contextDir, builder.DefaultDockerfileName, dockerfileContents, 0777)
func TestDownloadRemote(t *testing.T) {
contextDir := fs.NewDir(t, "test-builder-download-remote",
fs.WithFile(builder.DefaultDockerfileName, dockerfileContents))
defer contextDir.Remove()

mux := http.NewServeMux()
server := httptest.NewServer(mux)
Expand All @@ -187,39 +186,15 @@ func TestMakeRemoteContext(t *testing.T) {
serverURL.Path = "/" + builder.DefaultDockerfileName
remoteURL := serverURL.String()

mux.Handle("/", http.FileServer(http.Dir(contextDir)))

remoteContext, err := MakeRemoteContext(remoteURL, map[string]func(io.ReadCloser) (io.ReadCloser, error){
mimeTypes.TextPlain: func(rc io.ReadCloser) (io.ReadCloser, error) {
dockerfile, err := ioutil.ReadAll(rc)
if err != nil {
return nil, err
}

r, err := archive.Generate(builder.DefaultDockerfileName, string(dockerfile))
if err != nil {
return nil, err
}
return ioutil.NopCloser(r), nil
},
})

if err != nil {
t.Fatalf("Error when executing DetectContextFromRemoteURL: %s", err)
}

if remoteContext == nil {
t.Fatal("Remote context should not be nil")
}
mux.Handle("/", http.FileServer(http.Dir(contextDir.Path())))

h, err := remoteContext.Hash(builder.DefaultDockerfileName)
if err != nil {
t.Fatalf("failed to compute hash %s", err)
}
contentType, content, err := downloadRemote(remoteURL)
require.NoError(t, err)

if expected, actual := "7b6b6b66bee9e2102fbdc2228be6c980a2a23adf371962a37286a49f7de0f7cc", h; expected != actual {
t.Fatalf("There should be file named %s %s in fileInfoSums", expected, actual)
}
assert.Equal(t, mimeTypes.TextPlain, contentType)
raw, err := ioutil.ReadAll(content)
require.NoError(t, err)
assert.Equal(t, dockerfileContents, string(raw))
}

func TestGetWithStatusError(t *testing.T) {
Expand Down

0 comments on commit 7c53e73

Please sign in to comment.