mirror of
https://github.com/containers/buildah.git
synced 2025-04-18 07:04:05 +03:00
Merge the two tar filters, if we need two, that we use when committing an image. Try to improve passing of error information from the writing end of a pipe to the reader, so that it can be reported better. Signed-off-by: Nalin Dahyabhai <nalin@redhat.com>
273 lines
6.6 KiB
Go
273 lines
6.6 KiB
Go
package buildah
|
|
|
|
import (
|
|
"archive/tar"
|
|
"errors"
|
|
"fmt"
|
|
"hash"
|
|
"io"
|
|
"sync"
|
|
"time"
|
|
|
|
digest "github.com/opencontainers/go-digest"
|
|
)
|
|
|
|
type digester interface {
|
|
io.WriteCloser
|
|
ContentType() string
|
|
Digest() digest.Digest
|
|
}
|
|
|
|
// A simple digester just digests its content as-is.
|
|
type simpleDigester struct {
|
|
digester digest.Digester
|
|
hasher hash.Hash
|
|
contentType string
|
|
}
|
|
|
|
func newSimpleDigester(contentType string) digester {
|
|
finalDigester := digest.Canonical.Digester()
|
|
return &simpleDigester{
|
|
digester: finalDigester,
|
|
hasher: finalDigester.Hash(),
|
|
contentType: contentType,
|
|
}
|
|
}
|
|
|
|
func (s *simpleDigester) ContentType() string {
|
|
return s.contentType
|
|
}
|
|
|
|
func (s *simpleDigester) Write(p []byte) (int, error) {
|
|
return s.hasher.Write(p)
|
|
}
|
|
|
|
func (s *simpleDigester) Close() error {
|
|
return nil
|
|
}
|
|
|
|
func (s *simpleDigester) Digest() digest.Digest {
|
|
return s.digester.Digest()
|
|
}
|
|
|
|
// A tarFilterer passes a tarball through to an io.WriteCloser, potentially
|
|
// modifying headers as it goes.
|
|
type tarFilterer struct {
|
|
wg sync.WaitGroup
|
|
pipeWriter *io.PipeWriter
|
|
closedLock sync.Mutex
|
|
closed bool
|
|
err error
|
|
}
|
|
|
|
func (t *tarFilterer) Write(p []byte) (int, error) {
|
|
return t.pipeWriter.Write(p)
|
|
}
|
|
|
|
func (t *tarFilterer) Close() error {
|
|
t.closedLock.Lock()
|
|
if t.closed {
|
|
t.closedLock.Unlock()
|
|
return errors.New("tar filter is already closed")
|
|
}
|
|
t.closed = true
|
|
t.closedLock.Unlock()
|
|
err := t.pipeWriter.Close()
|
|
t.wg.Wait()
|
|
if err != nil {
|
|
return fmt.Errorf("closing filter pipe: %w", err)
|
|
}
|
|
return t.err
|
|
}
|
|
|
|
// newTarFilterer passes one or more tar archives through to an io.WriteCloser
|
|
// as a single archive, potentially calling filter to modify headers and
|
|
// contents as it goes.
|
|
//
|
|
// Note: if "filter" indicates that a given item should be skipped, there is no
|
|
// guarantee that there will not be a subsequent item of type TypeLink, which
|
|
// is a hard link, which points to the skipped item as the link target.
|
|
func newTarFilterer(writeCloser io.WriteCloser, filter func(hdr *tar.Header) (skip, replaceContents bool, replacementContents io.Reader)) io.WriteCloser {
|
|
pipeReader, pipeWriter := io.Pipe()
|
|
tarWriter := tar.NewWriter(writeCloser)
|
|
filterer := &tarFilterer{
|
|
pipeWriter: pipeWriter,
|
|
}
|
|
filterer.wg.Add(1)
|
|
go func() {
|
|
filterer.closedLock.Lock()
|
|
closed := filterer.closed
|
|
filterer.closedLock.Unlock()
|
|
for !closed {
|
|
tarReader := tar.NewReader(pipeReader)
|
|
hdr, err := tarReader.Next()
|
|
for err == nil {
|
|
var skip, replaceContents bool
|
|
var replacementContents io.Reader
|
|
if filter != nil {
|
|
skip, replaceContents, replacementContents = filter(hdr)
|
|
}
|
|
if !skip {
|
|
err = tarWriter.WriteHeader(hdr)
|
|
if err != nil {
|
|
err = fmt.Errorf("filtering tar header for %q: %w", hdr.Name, err)
|
|
break
|
|
}
|
|
if hdr.Size != 0 {
|
|
var n int64
|
|
var copyErr error
|
|
if replaceContents {
|
|
n, copyErr = io.CopyN(tarWriter, replacementContents, hdr.Size)
|
|
} else {
|
|
n, copyErr = io.Copy(tarWriter, tarReader)
|
|
}
|
|
if copyErr != nil {
|
|
err = fmt.Errorf("copying content for %q: %w", hdr.Name, copyErr)
|
|
break
|
|
}
|
|
if n != hdr.Size {
|
|
err = fmt.Errorf("filtering content for %q: expected %d bytes, got %d bytes", hdr.Name, hdr.Size, n)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
hdr, err = tarReader.Next()
|
|
}
|
|
if err != io.EOF {
|
|
filterer.err = fmt.Errorf("reading tar archive: %w", err)
|
|
break
|
|
}
|
|
filterer.closedLock.Lock()
|
|
closed = filterer.closed
|
|
filterer.closedLock.Unlock()
|
|
}
|
|
err1 := tarWriter.Close()
|
|
err := writeCloser.Close()
|
|
if err == nil {
|
|
err = err1
|
|
}
|
|
pipeReader.CloseWithError(err)
|
|
filterer.wg.Done()
|
|
}()
|
|
return filterer
|
|
}
|
|
|
|
// A tar digester digests an archive, modifying the headers it digests by
|
|
// calling a specified function to potentially modify the header that it's
|
|
// about to write.
|
|
type tarDigester struct {
|
|
isOpen bool
|
|
nested digester
|
|
tarFilterer io.WriteCloser
|
|
}
|
|
|
|
func modifyTarHeaderForDigesting(hdr *tar.Header) (skip, replaceContents bool, replacementContents io.Reader) {
|
|
zeroTime := time.Time{}
|
|
hdr.ModTime = zeroTime
|
|
hdr.AccessTime = zeroTime
|
|
hdr.ChangeTime = zeroTime
|
|
return false, false, nil
|
|
}
|
|
|
|
func newTarDigester(contentType string) digester {
|
|
nested := newSimpleDigester(contentType)
|
|
digester := &tarDigester{
|
|
isOpen: true,
|
|
nested: nested,
|
|
tarFilterer: newTarFilterer(nested, modifyTarHeaderForDigesting),
|
|
}
|
|
return digester
|
|
}
|
|
|
|
func (t *tarDigester) ContentType() string {
|
|
return t.nested.ContentType()
|
|
}
|
|
|
|
func (t *tarDigester) Digest() digest.Digest {
|
|
return t.nested.Digest()
|
|
}
|
|
|
|
func (t *tarDigester) Write(p []byte) (int, error) {
|
|
return t.tarFilterer.Write(p)
|
|
}
|
|
|
|
func (t *tarDigester) Close() error {
|
|
if t.isOpen {
|
|
t.isOpen = false
|
|
return t.tarFilterer.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CompositeDigester can compute a digest over multiple items.
|
|
type CompositeDigester struct {
|
|
digesters []digester
|
|
closer io.Closer
|
|
}
|
|
|
|
// closeOpenDigester closes an open sub-digester, if we have one.
|
|
func (c *CompositeDigester) closeOpenDigester() {
|
|
if c.closer != nil {
|
|
c.closer.Close()
|
|
c.closer = nil
|
|
}
|
|
}
|
|
|
|
// Restart clears all state, so that the composite digester can start over.
|
|
func (c *CompositeDigester) Restart() {
|
|
c.closeOpenDigester()
|
|
c.digesters = nil
|
|
}
|
|
|
|
// Start starts recording the digest for a new item ("", "file", or "dir").
|
|
// The caller should call Hash() immediately after to retrieve the new
|
|
// io.WriteCloser.
|
|
func (c *CompositeDigester) Start(contentType string) {
|
|
c.closeOpenDigester()
|
|
switch contentType {
|
|
case "":
|
|
c.digesters = append(c.digesters, newSimpleDigester(""))
|
|
case "file", "dir":
|
|
digester := newTarDigester(contentType)
|
|
c.closer = digester
|
|
c.digesters = append(c.digesters, digester)
|
|
default:
|
|
panic(fmt.Sprintf(`unrecognized content type: expected "", "file", or "dir", got %q`, contentType))
|
|
}
|
|
}
|
|
|
|
// Hash returns the hasher for the current item.
|
|
func (c *CompositeDigester) Hash() io.WriteCloser {
|
|
num := len(c.digesters)
|
|
if num == 0 {
|
|
return nil
|
|
}
|
|
return c.digesters[num-1]
|
|
}
|
|
|
|
// Digest returns the content type and a composite digest over everything
|
|
// that's been digested.
|
|
func (c *CompositeDigester) Digest() (string, digest.Digest) {
|
|
c.closeOpenDigester()
|
|
num := len(c.digesters)
|
|
switch num {
|
|
case 0:
|
|
return "", ""
|
|
case 1:
|
|
return c.digesters[0].ContentType(), c.digesters[0].Digest()
|
|
default:
|
|
content := ""
|
|
for i, digester := range c.digesters {
|
|
if i > 0 {
|
|
content += ","
|
|
}
|
|
contentType := digester.ContentType()
|
|
if contentType != "" {
|
|
contentType += ":"
|
|
}
|
|
content += contentType + digester.Digest().Encoded()
|
|
}
|
|
return "multi", digest.Canonical.FromString(content)
|
|
}
|
|
}
|