Leave out of the build context what the app's .dockerignore names (closes #274)
Check / check (pull_request) Successful in 3m47s
Check / check (pull_request) Successful in 3m47s
Docker does not apply an app's `.dockerignore` to a build context sent as a tar, which is how upaas sends it, so every file in the clone, `.git/config` included, reached the build. upaas now reads the ignore file as `docker build` does, with the `ignorefile` reader of `github.com/moby/patternmatcher`, and leaves those files out of the tar: an ignore file named after the Dockerfile and next to it, such as `Dockerfile.dockerignore`, otherwise `.dockerignore` at the root of the clone. The Dockerfile path is read as a path inside the clone, and the Dockerfile (or the lowercase `dockerfile` Docker builds when `Dockerfile` is missing) and the ignore file always stay in. An app without an ignore file builds as before. Not handled: a Dockerfile that is a symlink to an ignored file fails the build. Model: opus-5-5 Co-authored-by: clawbot <sneak+clawbot@sneak.cloud>
This commit was merged in pull request #275.
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/moby/patternmatcher"
|
||||
"github.com/moby/patternmatcher/ignorefile"
|
||||
)
|
||||
|
||||
// defaultDockerfileName is the Dockerfile Docker builds when none is named.
|
||||
const defaultDockerfileName = "Dockerfile"
|
||||
|
||||
// defaultDockerignoreName is the ignore file at the root of a build context,
|
||||
// read when the Dockerfile has no ignore file of its own.
|
||||
const defaultDockerignoreName = ".dockerignore"
|
||||
|
||||
// tarBuildContext returns a tar of the build context in contextDir that leaves
|
||||
// out the files the app's ignore file names, as docker build does; Docker does
|
||||
// not apply the ignore file to a build context sent as a tar. dockerfile is
|
||||
// the path of the Dockerfile inside contextDir.
|
||||
func tarBuildContext(contextDir, dockerfile string) (io.ReadCloser, error) {
|
||||
excludes, err := readDockerignore(contextDir, dockerfile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return archive.TarWithOptions(contextDir, &archive.TarOptions{
|
||||
ExcludePatterns: excludes,
|
||||
})
|
||||
}
|
||||
|
||||
// readDockerignore returns the patterns of the files to leave out of the
|
||||
// build context in contextDir, read as docker build reads them for the
|
||||
// Dockerfile at dockerfile: from <dockerfile>.dockerignore next to the
|
||||
// Dockerfile if there is one, otherwise from .dockerignore at the root of the
|
||||
// context. Without either file there are no patterns.
|
||||
func readDockerignore(contextDir, dockerfile string) ([]string, error) {
|
||||
// Docker reads the Dockerfile path as a path inside the build context:
|
||||
// cleaned, with a leading / and any .. that would lead out of the context
|
||||
// dropped, so ./Dockerfile and /Dockerfile both name the Dockerfile at the
|
||||
// root.
|
||||
dockerfile = strings.TrimPrefix(filepath.Join("/", dockerfile), "/")
|
||||
if dockerfile == "" {
|
||||
dockerfile = defaultDockerfileName
|
||||
}
|
||||
|
||||
// Reading through os.Root keeps the read inside the build context, even
|
||||
// when the ignore file is a symlink.
|
||||
root, err := os.OpenRoot(contextDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() { _ = root.Close() }()
|
||||
|
||||
// When a Dockerfile named Dockerfile is missing, Docker builds a
|
||||
// lowercase dockerfile in the same directory instead, and docker build
|
||||
// then reads dockerfile.dockerignore as its ignore file.
|
||||
if filepath.Base(dockerfile) == defaultDockerfileName {
|
||||
lowercase := filepath.Join(filepath.Dir(dockerfile), "dockerfile")
|
||||
|
||||
_, dockerfileErr := root.Lstat(dockerfile)
|
||||
_, lowercaseErr := root.Lstat(lowercase)
|
||||
|
||||
if errors.Is(dockerfileErr, fs.ErrNotExist) && lowercaseErr == nil {
|
||||
dockerfile = lowercase
|
||||
}
|
||||
}
|
||||
|
||||
name := dockerfile + defaultDockerignoreName
|
||||
|
||||
file, err := root.Open(name)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
name = defaultDockerignoreName
|
||||
file, err = root.Open(name)
|
||||
}
|
||||
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read %s: %w", name, err)
|
||||
}
|
||||
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
excludes, err := ignorefile.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read %s: %w", name, err)
|
||||
}
|
||||
|
||||
// Like the docker command line, never leave out .dockerignore or the
|
||||
// Dockerfile: Docker reads the Dockerfile from the context.
|
||||
for _, keep := range []string{defaultDockerignoreName, filepath.ToSlash(dockerfile)} {
|
||||
excluded, err := patternmatcher.MatchesOrParentMatches(keep, excludes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid pattern in %s: %w", name, err)
|
||||
}
|
||||
|
||||
if excluded {
|
||||
excludes = append(excludes, "!"+keep)
|
||||
}
|
||||
}
|
||||
|
||||
return excludes, nil
|
||||
}
|
||||
Reference in New Issue
Block a user