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>
115 lines
3.5 KiB
Go
115 lines
3.5 KiB
Go
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
|
|
}
|