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 .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() }() 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 }