1
0
mirror of https://github.com/containers/skopeo.git synced 2025-05-04 22:16:43 +00:00
skopeo/vendor/github.com/containers/storage/check.go
renovate[bot] fa1762f52b fix(deps): update module github.com/containers/image/v5 to v5.33.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Miloslav Trmač <mitr@redhat.com>
2024-11-12 20:34:31 +01:00

1159 lines
42 KiB
Go

package storage
import (
"archive/tar"
"errors"
"fmt"
"io"
"os"
"path"
"path/filepath"
"slices"
"sort"
"strings"
"sync"
"time"
drivers "github.com/containers/storage/drivers"
"github.com/containers/storage/pkg/archive"
"github.com/containers/storage/pkg/idtools"
"github.com/containers/storage/pkg/ioutils"
"github.com/containers/storage/types"
"github.com/sirupsen/logrus"
)
var (
// ErrLayerUnaccounted describes a layer that is present in the lower-level storage driver,
// but which is not known to or managed by the higher-level driver-agnostic logic.
ErrLayerUnaccounted = types.ErrLayerUnaccounted
// ErrLayerUnreferenced describes a layer which is not used by any image or container.
ErrLayerUnreferenced = types.ErrLayerUnreferenced
// ErrLayerIncorrectContentDigest describes a layer for which the contents of one or more
// files which were added in the layer appear to have changed. It may instead look like an
// unnamed "file integrity checksum failed" error.
ErrLayerIncorrectContentDigest = types.ErrLayerIncorrectContentDigest
// ErrLayerIncorrectContentSize describes a layer for which regenerating the diff that was
// used to populate the layer produced a diff of a different size. We check the digest
// first, so it's highly unlikely you'll ever see this error.
ErrLayerIncorrectContentSize = types.ErrLayerIncorrectContentSize
// ErrLayerContentModified describes a layer which contains contents which should not be
// there, or for which ownership/permissions/dates have been changed.
ErrLayerContentModified = types.ErrLayerContentModified
// ErrLayerDataMissing describes a layer which is missing a big data item.
ErrLayerDataMissing = types.ErrLayerDataMissing
// ErrLayerMissing describes a layer which is the missing parent of a layer.
ErrLayerMissing = types.ErrLayerMissing
// ErrImageLayerMissing describes an image which claims to have a layer that we don't know
// about.
ErrImageLayerMissing = types.ErrImageLayerMissing
// ErrImageDataMissing describes an image which is missing a big data item.
ErrImageDataMissing = types.ErrImageDataMissing
// ErrImageDataIncorrectSize describes an image which has a big data item which looks like
// its size has changed, likely because it's been modified somehow.
ErrImageDataIncorrectSize = types.ErrImageDataIncorrectSize
// ErrContainerImageMissing describes a container which claims to be based on an image that
// we don't know about.
ErrContainerImageMissing = types.ErrContainerImageMissing
// ErrContainerDataMissing describes a container which is missing a big data item.
ErrContainerDataMissing = types.ErrContainerDataMissing
// ErrContainerDataIncorrectSize describes a container which has a big data item which looks
// like its size has changed, likely because it's been modified somehow.
ErrContainerDataIncorrectSize = types.ErrContainerDataIncorrectSize
)
const (
defaultMaximumUnreferencedLayerAge = 24 * time.Hour
)
// CheckOptions is the set of options for Check(), specifying which tests to perform.
type CheckOptions struct {
LayerUnreferencedMaximumAge *time.Duration // maximum allowed age of unreferenced layers
LayerDigests bool // check that contents of image layer diffs can still be reconstructed
LayerMountable bool // check that layers are mountable
LayerContents bool // check that contents of image layers match their diffs, with no unexpected changes, requires LayerMountable
LayerData bool // check that associated "big" data items are present and can be read
ImageData bool // check that associated "big" data items are present, can be read, and match the recorded size
ContainerData bool // check that associated "big" data items are present and can be read
}
// checkIgnore is used to tell functions that compare the contents of a mounted
// layer to the contents that we'd expect it to have to ignore certain
// discrepancies
type checkIgnore struct {
ownership, timestamps, permissions bool
}
// CheckMost returns a CheckOptions with mostly just "quick" checks enabled.
func CheckMost() *CheckOptions {
return &CheckOptions{
LayerDigests: true,
LayerMountable: true,
LayerContents: false,
LayerData: true,
ImageData: true,
ContainerData: true,
}
}
// CheckEverything returns a CheckOptions with every check enabled.
func CheckEverything() *CheckOptions {
return &CheckOptions{
LayerDigests: true,
LayerMountable: true,
LayerContents: true,
LayerData: true,
ImageData: true,
ContainerData: true,
}
}
// CheckReport is a list of detected problems.
type CheckReport struct {
Layers map[string][]error // damaged read-write layers
ROLayers map[string][]error // damaged read-only layers
layerParentsByLayerID map[string]string
layerOrder map[string]int
Images map[string][]error // damaged read-write images (including those with damaged layers)
ROImages map[string][]error // damaged read-only images (including those with damaged layers)
Containers map[string][]error // damaged containers (including those based on damaged images)
}
// RepairOptions is the set of options for Repair().
type RepairOptions struct {
RemoveContainers bool // Remove damaged containers
}
// RepairEverything returns a RepairOptions with every optional remediation
// enabled.
func RepairEverything() *RepairOptions {
return &RepairOptions{
RemoveContainers: true,
}
}
// Check returns a list of problems with what's in the store, as a whole. It can be very expensive
// to call.
func (s *store) Check(options *CheckOptions) (CheckReport, error) {
var ignore checkIgnore
for _, o := range s.graphOptions {
if strings.Contains(o, "ignore_chown_errors=true") {
ignore.ownership = true
}
if strings.HasPrefix(o, "force_mask=") {
ignore.permissions = true
}
}
for o := range s.pullOptions {
if strings.Contains(o, "use_hard_links") {
if s.pullOptions[o] == "true" {
ignore.timestamps = true
}
}
}
if options == nil {
options = CheckMost()
}
report := CheckReport{
Layers: make(map[string][]error),
ROLayers: make(map[string][]error),
layerParentsByLayerID: make(map[string]string), // layers ID -> their parent's ID, if there is one
layerOrder: make(map[string]int), // layers ID -> order for removal, if we needed to remove them all
Images: make(map[string][]error),
ROImages: make(map[string][]error),
Containers: make(map[string][]error),
}
// This map will track known layer IDs. If we have multiple stores, read-only ones can
// contain copies of layers that are in the read-write store, but we'll only ever be
// mounting or extracting contents from the read-write versions, since we always search it
// first. The boolean will track if the layer is referenced by at least one image or
// container.
referencedLayers := make(map[string]bool)
referencedROLayers := make(map[string]bool)
// This map caches the headers for items included in layer diffs.
diffHeadersByLayer := make(map[string][]*tar.Header)
var diffHeadersByLayerMutex sync.Mutex
// Walk the list of layer stores, looking at each layer that we didn't see in a
// previously-visited store.
if _, _, err := readOrWriteAllLayerStores(s, func(store roLayerStore) (struct{}, bool, error) {
layers, err := store.Layers()
if err != nil {
return struct{}{}, true, err
}
isReadWrite := roLayerStoreIsReallyReadWrite(store)
readWriteDesc := ""
if !isReadWrite {
readWriteDesc = "read-only "
}
// Examine each layer in turn.
for i := range layers {
layer := layers[i]
id := layer.ID
// If we've already seen a layer with this ID, no need to process it again.
if _, checked := referencedLayers[id]; checked {
continue
}
if _, checked := referencedROLayers[id]; checked {
continue
}
// Note the parent of this layer, and add it to the map of known layers so
// that we know that we've visited it, but we haven't confirmed that it's
// used by anything.
report.layerParentsByLayerID[id] = layer.Parent
if isReadWrite {
referencedLayers[id] = false
} else {
referencedROLayers[id] = false
}
logrus.Debugf("checking %slayer %s", readWriteDesc, id)
// Check that all of the big data items are present and can be read. We
// have no digest or size information to compare the contents to (grumble),
// so we can't verify that the contents haven't been changed since they
// were stored.
if options.LayerData {
for _, name := range layer.BigDataNames {
func() {
rc, err := store.BigData(id, name)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
err := fmt.Errorf("%slayer %s: data item %q: %w", readWriteDesc, id, name, ErrLayerDataMissing)
if isReadWrite {
report.Layers[id] = append(report.Layers[id], err)
} else {
report.ROLayers[id] = append(report.ROLayers[id], err)
}
return
}
err = fmt.Errorf("%slayer %s: data item %q: %w", readWriteDesc, id, name, err)
if isReadWrite {
report.Layers[id] = append(report.Layers[id], err)
} else {
report.ROLayers[id] = append(report.ROLayers[id], err)
}
return
}
defer rc.Close()
if _, err = io.Copy(io.Discard, rc); err != nil {
err = fmt.Errorf("%slayer %s: data item %q: %w", readWriteDesc, id, name, err)
if isReadWrite {
report.Layers[id] = append(report.Layers[id], err)
} else {
report.ROLayers[id] = append(report.ROLayers[id], err)
}
return
}
}()
}
}
// Check that the content we get back when extracting the layer's contents
// match the recorded digest and size. A layer for which they're not given
// isn't a part of an image, and is likely the read-write layer for a
// container, and we can't vouch for the integrity of its contents.
// For each layer with known contents, record the headers for the layer's
// diff, which we can use to reconstruct the expected contents for the tree
// we see when the layer is mounted.
if options.LayerDigests && layer.UncompressedDigest != "" {
func() {
expectedDigest := layer.UncompressedDigest
// Double-check that the digest isn't invalid somehow.
if err := layer.UncompressedDigest.Validate(); err != nil {
err := fmt.Errorf("%slayer %s: %w", readWriteDesc, id, err)
if isReadWrite {
report.Layers[id] = append(report.Layers[id], err)
} else {
report.ROLayers[id] = append(report.ROLayers[id], err)
}
return
}
// Extract the diff.
uncompressed := archive.Uncompressed
diffOptions := DiffOptions{
Compression: &uncompressed,
}
diff, err := store.Diff("", id, &diffOptions)
if err != nil {
err := fmt.Errorf("%slayer %s: %w", readWriteDesc, id, err)
if isReadWrite {
report.Layers[id] = append(report.Layers[id], err)
} else {
report.ROLayers[id] = append(report.ROLayers[id], err)
}
return
}
// Digest and count the length of the diff.
digester := expectedDigest.Algorithm().Digester()
counter := ioutils.NewWriteCounter(digester.Hash())
reader := io.TeeReader(diff, counter)
var wg sync.WaitGroup
var archiveErr error
wg.Add(1)
go func(layerID string, diffReader io.Reader) {
// Read the diff, one item at a time.
tr := tar.NewReader(diffReader)
hdr, err := tr.Next()
for err == nil {
diffHeadersByLayerMutex.Lock()
diffHeadersByLayer[layerID] = append(diffHeadersByLayer[layerID], hdr)
diffHeadersByLayerMutex.Unlock()
hdr, err = tr.Next()
}
if !errors.Is(err, io.EOF) {
archiveErr = err
}
// consume any trailer after the EOF marker
if _, err := io.Copy(io.Discard, diffReader); err != nil {
err = fmt.Errorf("layer %s: consume any trailer after the EOF marker: %w", layerID, err)
if isReadWrite {
report.Layers[layerID] = append(report.Layers[layerID], err)
} else {
report.ROLayers[layerID] = append(report.ROLayers[layerID], err)
}
}
wg.Done()
}(id, reader)
wg.Wait()
diff.Close()
if archiveErr != nil {
// Reading the diff didn't end as expected
diffHeadersByLayerMutex.Lock()
delete(diffHeadersByLayer, id)
diffHeadersByLayerMutex.Unlock()
archiveErr = fmt.Errorf("%slayer %s: %w", readWriteDesc, id, archiveErr)
if isReadWrite {
report.Layers[id] = append(report.Layers[id], archiveErr)
} else {
report.ROLayers[id] = append(report.ROLayers[id], archiveErr)
}
return
}
if digester.Digest() != layer.UncompressedDigest {
// The diff digest didn't match.
diffHeadersByLayerMutex.Lock()
delete(diffHeadersByLayer, id)
diffHeadersByLayerMutex.Unlock()
err := fmt.Errorf("%slayer %s: %w", readWriteDesc, id, ErrLayerIncorrectContentDigest)
if isReadWrite {
report.Layers[id] = append(report.Layers[id], err)
} else {
report.ROLayers[id] = append(report.ROLayers[id], err)
}
}
if layer.UncompressedSize != -1 && counter.Count != layer.UncompressedSize {
// We expected the diff to have a specific size, and
// it didn't match.
diffHeadersByLayerMutex.Lock()
delete(diffHeadersByLayer, id)
diffHeadersByLayerMutex.Unlock()
err := fmt.Errorf("%slayer %s: read %d bytes instead of %d bytes: %w", readWriteDesc, id, counter.Count, layer.UncompressedSize, ErrLayerIncorrectContentSize)
if isReadWrite {
report.Layers[id] = append(report.Layers[id], err)
} else {
report.ROLayers[id] = append(report.ROLayers[id], err)
}
}
}()
}
}
// At this point we're out of things that we can be sure will work in read-only
// stores, so skip the rest for any stores that aren't also read-write stores.
if !isReadWrite {
return struct{}{}, false, nil
}
// Content and mount checks are also things that we can only be sure will work in
// read-write stores.
for i := range layers {
layer := layers[i]
id := layer.ID
// Compare to what we see when we mount the layer and walk the tree, and
// flag cases where content is in the layer that shouldn't be there. The
// tar-split implementation of Diff() won't catch this problem by itself.
if options.LayerMountable {
func() {
// Mount the layer.
mountPoint, err := s.graphDriver.Get(id, drivers.MountOpts{MountLabel: layer.MountLabel, Options: []string{"ro"}})
if err != nil {
err := fmt.Errorf("%slayer %s: %w", readWriteDesc, id, err)
if isReadWrite {
report.Layers[id] = append(report.Layers[id], err)
} else {
report.ROLayers[id] = append(report.ROLayers[id], err)
}
return
}
// Unmount the layer when we're done in here.
defer func() {
if err := s.graphDriver.Put(id); err != nil {
err := fmt.Errorf("%slayer %s: %w", readWriteDesc, id, err)
if isReadWrite {
report.Layers[id] = append(report.Layers[id], err)
} else {
report.ROLayers[id] = append(report.ROLayers[id], err)
}
return
}
}()
// If we're not looking at layer contents, or we didn't
// look at the diff for this layer, we're done here.
if !options.LayerDigests || layer.UncompressedDigest == "" || !options.LayerContents {
return
}
// Build a list of all of the changes in all of the layers
// that make up the tree we're looking at.
diffHeaderSet := [][]*tar.Header{}
// If we don't know _all_ of the changes that produced this
// layer, it's not part of an image, so we're done here.
for layerID := id; layerID != ""; layerID = report.layerParentsByLayerID[layerID] {
diffHeadersByLayerMutex.Lock()
layerChanges, haveChanges := diffHeadersByLayer[layerID]
diffHeadersByLayerMutex.Unlock()
if !haveChanges {
return
}
// The diff headers for this layer go _before_ those of
// layers that inherited some of its contents.
diffHeaderSet = append([][]*tar.Header{layerChanges}, diffHeaderSet...)
}
expectedCheckDirectory := newCheckDirectoryDefaults()
for _, diffHeaders := range diffHeaderSet {
expectedCheckDirectory.headers(diffHeaders)
}
// Scan the directory tree under the mount point.
var idmap *idtools.IDMappings
if !s.canUseShifting(layer.UIDMap, layer.GIDMap) {
// we would have had to chown() layer contents to match ID maps
idmap = idtools.NewIDMappingsFromMaps(layer.UIDMap, layer.GIDMap)
}
actualCheckDirectory, err := newCheckDirectoryFromDirectory(mountPoint)
if err != nil {
err := fmt.Errorf("scanning contents of %slayer %s: %w", readWriteDesc, id, err)
if isReadWrite {
report.Layers[id] = append(report.Layers[id], err)
} else {
report.ROLayers[id] = append(report.ROLayers[id], err)
}
return
}
// Every departure from our expectations is an error.
diffs := compareCheckDirectory(expectedCheckDirectory, actualCheckDirectory, idmap, ignore)
for _, diff := range diffs {
err := fmt.Errorf("%slayer %s: %s, %w", readWriteDesc, id, diff, ErrLayerContentModified)
if isReadWrite {
report.Layers[id] = append(report.Layers[id], err)
} else {
report.ROLayers[id] = append(report.ROLayers[id], err)
}
}
}()
}
}
// Check that we don't have any dangling parent layer references.
for id, parent := range report.layerParentsByLayerID {
// If this layer doesn't have a parent, no problem.
if parent == "" {
continue
}
// If we've already seen a layer with this parent ID, skip it.
if _, checked := referencedLayers[parent]; checked {
continue
}
if _, checked := referencedROLayers[parent]; checked {
continue
}
// We haven't seen a layer with the ID that this layer's record
// says is its parent's ID.
err := fmt.Errorf("%slayer %s: %w", readWriteDesc, parent, ErrLayerMissing)
report.Layers[id] = append(report.Layers[id], err)
}
return struct{}{}, false, nil
}); err != nil {
return CheckReport{}, err
}
// This map will track examined images. If we have multiple stores, read-only ones can
// contain copies of images that are also in the read-write store, or the read-write store
// may contain a duplicate entry that refers to layers in the read-only stores, but when
// trying to export them, we only look at the first copy of the image.
examinedImages := make(map[string]struct{})
// Walk the list of image stores, looking at each image that we didn't see in a
// previously-visited store.
if _, _, err := readAllImageStores(s, func(store roImageStore) (struct{}, bool, error) {
images, err := store.Images()
if err != nil {
return struct{}{}, true, err
}
isReadWrite := roImageStoreIsReallyReadWrite(store)
readWriteDesc := ""
if !isReadWrite {
readWriteDesc = "read-only "
}
// Examine each image in turn.
for i := range images {
image := images[i]
id := image.ID
// If we've already seen an image with this ID, skip it.
if _, checked := examinedImages[id]; checked {
continue
}
examinedImages[id] = struct{}{}
logrus.Debugf("checking %simage %s", readWriteDesc, id)
if options.ImageData {
// Check that all of the big data items are present and reading them
// back gives us the right amount of data. Even though we record
// digests that can be used to look them up, we don't know how they
// were calculated (they're only used as lookup keys), so do not try
// to check them.
for _, key := range image.BigDataNames {
func() {
data, err := store.BigData(id, key)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
err = fmt.Errorf("%simage %s: data item %q: %w", readWriteDesc, id, key, ErrImageDataMissing)
if isReadWrite {
report.Images[id] = append(report.Images[id], err)
} else {
report.ROImages[id] = append(report.ROImages[id], err)
}
return
}
err = fmt.Errorf("%simage %s: data item %q: %w", readWriteDesc, id, key, err)
if isReadWrite {
report.Images[id] = append(report.Images[id], err)
} else {
report.ROImages[id] = append(report.ROImages[id], err)
}
return
}
if int64(len(data)) != image.BigDataSizes[key] {
err = fmt.Errorf("%simage %s: data item %q: %w", readWriteDesc, id, key, ErrImageDataIncorrectSize)
if isReadWrite {
report.Images[id] = append(report.Images[id], err)
} else {
report.ROImages[id] = append(report.ROImages[id], err)
}
return
}
}()
}
}
// Walk the layers list for the image. For every layer that the image uses
// that has errors, the layer's errors are also the image's errors.
examinedImageLayers := make(map[string]struct{})
for _, topLayer := range append([]string{image.TopLayer}, image.MappedTopLayers...) {
if topLayer == "" {
continue
}
if _, checked := examinedImageLayers[topLayer]; checked {
continue
}
examinedImageLayers[topLayer] = struct{}{}
for layer := topLayer; layer != ""; layer = report.layerParentsByLayerID[layer] {
// The referenced layer should have a corresponding entry in
// one map or the other.
_, checked := referencedLayers[layer]
_, checkedRO := referencedROLayers[layer]
if !checked && !checkedRO {
err := fmt.Errorf("layer %s: %w", layer, ErrImageLayerMissing)
err = fmt.Errorf("%simage %s: %w", readWriteDesc, id, err)
if isReadWrite {
report.Images[id] = append(report.Images[id], err)
} else {
report.ROImages[id] = append(report.ROImages[id], err)
}
} else {
// Count this layer as referenced. Whether by the
// image or one of its child layers doesn't matter
// at this point.
if _, ok := referencedLayers[layer]; ok {
referencedLayers[layer] = true
}
if _, ok := referencedROLayers[layer]; ok {
referencedROLayers[layer] = true
}
}
if isReadWrite {
if len(report.Layers[layer]) > 0 {
report.Images[id] = append(report.Images[id], report.Layers[layer]...)
}
if len(report.ROLayers[layer]) > 0 {
report.Images[id] = append(report.Images[id], report.ROLayers[layer]...)
}
} else {
if len(report.Layers[layer]) > 0 {
report.ROImages[id] = append(report.ROImages[id], report.Layers[layer]...)
}
if len(report.ROLayers[layer]) > 0 {
report.ROImages[id] = append(report.ROImages[id], report.ROLayers[layer]...)
}
}
}
}
}
return struct{}{}, false, nil
}); err != nil {
return CheckReport{}, err
}
// Iterate over each container in turn.
if _, _, err := readContainerStore(s, func() (struct{}, bool, error) {
containers, err := s.containerStore.Containers()
if err != nil {
return struct{}{}, true, err
}
for i := range containers {
container := containers[i]
id := container.ID
logrus.Debugf("checking container %s", id)
if options.ContainerData {
// Check that all of the big data items are present and reading them
// back gives us the right amount of data.
for _, key := range container.BigDataNames {
func() {
data, err := s.containerStore.BigData(id, key)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
err = fmt.Errorf("container %s: data item %q: %w", id, key, ErrContainerDataMissing)
report.Containers[id] = append(report.Containers[id], err)
return
}
err = fmt.Errorf("container %s: data item %q: %w", id, key, err)
report.Containers[id] = append(report.Containers[id], err)
return
}
if int64(len(data)) != container.BigDataSizes[key] {
err = fmt.Errorf("container %s: data item %q: %w", id, key, ErrContainerDataIncorrectSize)
report.Containers[id] = append(report.Containers[id], err)
return
}
}()
}
}
// Look at the container's base image. If the image has errors, the image's errors
// are the container's errors.
if container.ImageID != "" {
if _, checked := examinedImages[container.ImageID]; !checked {
err := fmt.Errorf("image %s: %w", container.ImageID, ErrContainerImageMissing)
report.Containers[id] = append(report.Containers[id], err)
}
if len(report.Images[container.ImageID]) > 0 {
report.Containers[id] = append(report.Containers[id], report.Images[container.ImageID]...)
}
if len(report.ROImages[container.ImageID]) > 0 {
report.Containers[id] = append(report.Containers[id], report.ROImages[container.ImageID]...)
}
}
// Count the container's layer as referenced.
if container.LayerID != "" {
referencedLayers[container.LayerID] = true
}
}
return struct{}{}, false, nil
}); err != nil {
return CheckReport{}, err
}
// Now go back through all of the layer stores, and flag any layers which don't belong
// to an image or a container, and has been around longer than we can reasonably expect
// such a layer to be present before a corresponding image record is added.
if _, _, err := readAllLayerStores(s, func(store roLayerStore) (struct{}, bool, error) {
if isReadWrite := roLayerStoreIsReallyReadWrite(store); !isReadWrite {
return struct{}{}, false, nil
}
layers, err := store.Layers()
if err != nil {
return struct{}{}, true, err
}
for _, layer := range layers {
maximumAge := defaultMaximumUnreferencedLayerAge
if options.LayerUnreferencedMaximumAge != nil {
maximumAge = *options.LayerUnreferencedMaximumAge
}
if referenced := referencedLayers[layer.ID]; !referenced {
if layer.Created.IsZero() || layer.Created.Add(maximumAge).Before(time.Now()) {
// Either we don't (and never will) know when this layer was
// created, or it was created far enough in the past that we're
// reasonably sure it's not part of an image that's being written
// right now.
err := fmt.Errorf("layer %s: %w", layer.ID, ErrLayerUnreferenced)
report.Layers[layer.ID] = append(report.Layers[layer.ID], err)
}
}
}
return struct{}{}, false, nil
}); err != nil {
return CheckReport{}, err
}
// If the driver can tell us about which layers it knows about, we should have previously
// examined all of them. Any that we didn't are probably just wasted space.
// Note: if the driver doesn't support enumerating layers, it returns ErrNotSupported.
if err := s.startUsingGraphDriver(); err != nil {
return CheckReport{}, err
}
defer s.stopUsingGraphDriver()
layerList, err := s.graphDriver.ListLayers()
if err != nil && !errors.Is(err, drivers.ErrNotSupported) {
return CheckReport{}, err
}
if !errors.Is(err, drivers.ErrNotSupported) {
for i, id := range layerList {
if _, known := referencedLayers[id]; !known {
err := fmt.Errorf("layer %s: %w", id, ErrLayerUnaccounted)
report.Layers[id] = append(report.Layers[id], err)
}
report.layerOrder[id] = i + 1
}
}
return report, nil
}
func roLayerStoreIsReallyReadWrite(store roLayerStore) bool {
return store.(*layerStore).lockfile.IsReadWrite()
}
func roImageStoreIsReallyReadWrite(store roImageStore) bool {
return store.(*imageStore).lockfile.IsReadWrite()
}
// Repair removes items which are themselves damaged, or which depend on items which are damaged.
// Errors are returned if an attempt to delete an item fails.
func (s *store) Repair(report CheckReport, options *RepairOptions) []error {
if options == nil {
options = RepairEverything()
}
var errs []error
// Just delete damaged containers.
if options.RemoveContainers {
for id := range report.Containers {
err := s.DeleteContainer(id)
if err != nil && !errors.Is(err, ErrContainerUnknown) {
err := fmt.Errorf("deleting container %s: %w", id, err)
errs = append(errs, err)
}
}
}
// Now delete damaged images. Note which layers were removed as part of removing those images.
deletedLayers := make(map[string]struct{})
for id := range report.Images {
layers, err := s.DeleteImage(id, true)
if err != nil {
if !errors.Is(err, ErrImageUnknown) && !errors.Is(err, ErrLayerUnknown) {
err := fmt.Errorf("deleting image %s: %w", id, err)
errs = append(errs, err)
}
} else {
for _, layer := range layers {
logrus.Debugf("deleted layer %s", layer)
deletedLayers[layer] = struct{}{}
}
logrus.Debugf("deleted image %s", id)
}
}
// Build a list of the layers that we need to remove, sorted with parents of layers before
// layers that they are parents of.
layersToDelete := make([]string, 0, len(report.Layers))
for id := range report.Layers {
layersToDelete = append(layersToDelete, id)
}
depth := func(id string) int {
d := 0
parent := report.layerParentsByLayerID[id]
for parent != "" {
d++
parent = report.layerParentsByLayerID[parent]
}
return d
}
isUnaccounted := func(errs []error) bool {
return slices.ContainsFunc(errs, func(err error) bool {
return errors.Is(err, ErrLayerUnaccounted)
})
}
sort.Slice(layersToDelete, func(i, j int) bool {
// we've not heard of either of them, so remove them in the order the driver suggested
if isUnaccounted(report.Layers[layersToDelete[i]]) &&
isUnaccounted(report.Layers[layersToDelete[j]]) &&
report.layerOrder[layersToDelete[i]] != 0 && report.layerOrder[layersToDelete[j]] != 0 {
return report.layerOrder[layersToDelete[i]] < report.layerOrder[layersToDelete[j]]
}
// always delete the one we've heard of first
if isUnaccounted(report.Layers[layersToDelete[i]]) && !isUnaccounted(report.Layers[layersToDelete[j]]) {
return false
}
// always delete the one we've heard of first
if !isUnaccounted(report.Layers[layersToDelete[i]]) && isUnaccounted(report.Layers[layersToDelete[j]]) {
return true
}
// we've heard of both of them; the one that's on the end of a longer chain goes first
return depth(layersToDelete[i]) > depth(layersToDelete[j]) // closer-to-a-notional-base layers get removed later
})
// Now delete the layers that haven't been removed along with images.
for _, id := range layersToDelete {
if _, ok := deletedLayers[id]; ok {
continue
}
for _, reportedErr := range report.Layers[id] {
var err error
// If a layer was unaccounted for, remove it at the storage driver level.
// Otherwise, remove it at the higher level and let the higher level
// logic worry about telling the storage driver to delete the layer.
if errors.Is(reportedErr, ErrLayerUnaccounted) {
if err = s.graphDriver.Remove(id); err != nil {
err = fmt.Errorf("deleting storage layer %s: %v", id, err)
} else {
logrus.Debugf("deleted storage layer %s", id)
}
} else {
var stillMounted bool
if stillMounted, err = s.Unmount(id, true); err == nil && !stillMounted {
logrus.Debugf("unmounted layer %s", id)
} else if err != nil {
logrus.Debugf("unmounting layer %s: %v", id, err)
} else {
logrus.Debugf("layer %s still mounted", id)
}
if err = s.DeleteLayer(id); err != nil {
err = fmt.Errorf("deleting layer %s: %w", id, err)
logrus.Debugf("deleted layer %s", id)
}
}
if err != nil && !errors.Is(err, ErrLayerUnknown) && !errors.Is(err, ErrNotALayer) && !errors.Is(err, os.ErrNotExist) {
errs = append(errs, err)
}
}
}
return errs
}
// compareFileInfo returns a string summarizing what's different between the two checkFileInfos
func compareFileInfo(a, b checkFileInfo, idmap *idtools.IDMappings, ignore checkIgnore) string {
var comparison []string
if a.typeflag != b.typeflag {
comparison = append(comparison, fmt.Sprintf("filetype:%v→%v", a.typeflag, b.typeflag))
}
if idmap != nil && !idmap.Empty() {
mappedUID, mappedGID, err := idmap.ToContainer(idtools.IDPair{UID: b.uid, GID: b.gid})
if err != nil {
return err.Error()
}
b.uid, b.gid = mappedUID, mappedGID
}
if a.uid != b.uid && !ignore.ownership {
comparison = append(comparison, fmt.Sprintf("uid:%d→%d", a.uid, b.uid))
}
if a.gid != b.gid && !ignore.ownership {
comparison = append(comparison, fmt.Sprintf("gid:%d→%d", a.gid, b.gid))
}
if a.size != b.size {
comparison = append(comparison, fmt.Sprintf("size:%d→%d", a.size, b.size))
}
if (os.ModeType|os.ModePerm)&a.mode != (os.ModeType|os.ModePerm)&b.mode && !ignore.permissions {
comparison = append(comparison, fmt.Sprintf("mode:%04o→%04o", a.mode, b.mode))
}
if a.mtime != b.mtime && !ignore.timestamps {
comparison = append(comparison, fmt.Sprintf("mtime:0x%x→0x%x", a.mtime, b.mtime))
}
return strings.Join(comparison, ",")
}
// checkFileInfo is what we care about for files
type checkFileInfo struct {
typeflag byte
uid, gid int
size int64
mode os.FileMode
mtime int64 // unix-style whole seconds
}
// checkDirectory is a node in a filesystem record, possibly the top
type checkDirectory struct {
directory map[string]*checkDirectory // subdirectories
file map[string]checkFileInfo // non-directories
checkFileInfo
}
// newCheckDirectory creates an empty checkDirectory
func newCheckDirectory(uid, gid int, size int64, mode os.FileMode, mtime int64) *checkDirectory {
return &checkDirectory{
directory: make(map[string]*checkDirectory),
file: make(map[string]checkFileInfo),
checkFileInfo: checkFileInfo{
typeflag: tar.TypeDir,
uid: uid,
gid: gid,
size: size,
mode: mode,
mtime: mtime,
},
}
}
// newCheckDirectoryDefaults creates an empty checkDirectory with hardwired defaults for the UID
// (0), GID (0), size (0) and permissions (0o555)
func newCheckDirectoryDefaults() *checkDirectory {
return newCheckDirectory(0, 0, 0, 0o555, time.Now().Unix())
}
// newCheckDirectoryFromDirectory creates a checkDirectory for an on-disk directory tree
func newCheckDirectoryFromDirectory(dir string) (*checkDirectory, error) {
cd := newCheckDirectoryDefaults()
err := filepath.Walk(dir, func(walkpath string, info os.FileInfo, err error) error {
if err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
rel, err := filepath.Rel(dir, walkpath)
if err != nil {
return err
}
hdr, err := tar.FileInfoHeader(info, "") // we don't record link targets, so don't bother looking it up
if err != nil {
return err
}
hdr.Name = filepath.ToSlash(rel)
cd.header(hdr)
return nil
})
if err != nil {
return nil, err
}
return cd, nil
}
// add adds an item to a checkDirectory
func (c *checkDirectory) add(path string, typeflag byte, uid, gid int, size int64, mode os.FileMode, mtime int64) {
components := strings.Split(path, "/")
if components[len(components)-1] == "" {
components = components[:len(components)-1]
}
if components[0] == "." {
components = components[1:]
}
if typeflag != tar.TypeReg {
size = 0
}
switch len(components) {
case 0:
c.uid = uid
c.gid = gid
c.mode = mode
c.mtime = mtime
return
case 1:
switch typeflag {
case tar.TypeDir:
delete(c.file, components[0])
// directory entries are mergers, not replacements
if _, present := c.directory[components[0]]; !present {
c.directory[components[0]] = newCheckDirectory(uid, gid, size, mode, mtime)
} else {
c.directory[components[0]].checkFileInfo = checkFileInfo{
typeflag: tar.TypeDir,
uid: uid,
gid: gid,
size: size,
mode: mode,
mtime: mtime,
}
}
case tar.TypeXGlobalHeader:
// ignore, since even though it looks like a valid pathname, it doesn't end
// up on the filesystem
default:
// treat these as TypeReg items
delete(c.directory, components[0])
c.file[components[0]] = checkFileInfo{
typeflag: typeflag,
uid: uid,
gid: gid,
size: size,
mode: mode,
mtime: mtime,
}
}
return
}
subdirectory := c.directory[components[0]]
if subdirectory == nil {
subdirectory = newCheckDirectory(uid, gid, size, mode, mtime)
c.directory[components[0]] = subdirectory
}
subdirectory.add(strings.Join(components[1:], "/"), typeflag, uid, gid, size, mode, mtime)
}
// remove removes an item from a checkDirectory
func (c *checkDirectory) remove(path string) {
components := strings.Split(path, "/")
if len(components) == 1 {
delete(c.directory, components[0])
delete(c.file, components[0])
return
}
subdirectory := c.directory[components[0]]
if subdirectory != nil {
subdirectory.remove(strings.Join(components[1:], "/"))
}
}
// header updates a checkDirectory using information from the passed-in header
func (c *checkDirectory) header(hdr *tar.Header) {
name := path.Clean(hdr.Name)
dir, base := path.Split(name)
if file, ok := strings.CutPrefix(base, archive.WhiteoutPrefix); ok {
if base == archive.WhiteoutOpaqueDir {
c.remove(path.Clean(dir))
c.add(path.Clean(dir), tar.TypeDir, hdr.Uid, hdr.Gid, hdr.Size, os.FileMode(hdr.Mode), hdr.ModTime.Unix())
} else {
c.remove(path.Join(dir, file))
}
} else {
if hdr.Typeflag == tar.TypeLink {
// look up the attributes of the target of the hard link
// n.b. by convention, Linkname is always relative to the
// root directory of the archive, which is not always the
// same as being relative to hdr.Name
directory := c
for _, component := range strings.Split(path.Clean(hdr.Linkname), "/") {
if component == "." || component == ".." {
continue
}
if subdir, ok := directory.directory[component]; ok {
directory = subdir
continue
}
if file, ok := directory.file[component]; ok {
hdr.Typeflag = file.typeflag
hdr.Uid = file.uid
hdr.Gid = file.gid
hdr.Size = file.size
hdr.Mode = int64(file.mode)
hdr.ModTime = time.Unix(file.mtime, 0)
}
break
}
}
c.add(name, hdr.Typeflag, hdr.Uid, hdr.Gid, hdr.Size, os.FileMode(hdr.Mode), hdr.ModTime.Unix())
}
}
// headers updates a checkDirectory using information from the passed-in header slice
func (c *checkDirectory) headers(hdrs []*tar.Header) {
hdrs = slices.Clone(hdrs)
// sort the headers from the diff to ensure that whiteouts appear
// before content when they both appear in the same directory, per
// https://github.com/opencontainers/image-spec/blob/main/layer.md#whiteouts
// and that hard links appear after other types of entries
sort.SliceStable(hdrs, func(i, j int) bool {
if hdrs[i].Typeflag != tar.TypeLink && hdrs[j].Typeflag == tar.TypeLink {
return true
}
if hdrs[i].Typeflag == tar.TypeLink && hdrs[j].Typeflag != tar.TypeLink {
return false
}
idir, ifile := path.Split(hdrs[i].Name)
jdir, jfile := path.Split(hdrs[j].Name)
if idir != jdir {
return hdrs[i].Name < hdrs[j].Name
}
if ifile == archive.WhiteoutOpaqueDir {
return true
}
if strings.HasPrefix(ifile, archive.WhiteoutPrefix) && !strings.HasPrefix(jfile, archive.WhiteoutPrefix) {
return true
}
return false
})
for _, hdr := range hdrs {
c.header(hdr)
}
}
// names provides a sorted list of the path names in the directory tree
func (c *checkDirectory) names() []string {
names := make([]string, 0, len(c.file)+len(c.directory))
for name := range c.file {
names = append(names, name)
}
for name, subdirectory := range c.directory {
names = append(names, name+"/")
for _, subname := range subdirectory.names() {
names = append(names, name+"/"+subname)
}
}
return names
}
// compareCheckSubdirectory walks two subdirectory trees and returns a list of differences
func compareCheckSubdirectory(path string, a, b *checkDirectory, idmap *idtools.IDMappings, ignore checkIgnore) []string {
var diff []string
if a == nil {
a = newCheckDirectoryDefaults()
}
if b == nil {
b = newCheckDirectoryDefaults()
}
for aname, adir := range a.directory {
if bdir, present := b.directory[aname]; !present {
// directory was removed
diff = append(diff, "-"+path+"/"+aname+"/")
diff = append(diff, compareCheckSubdirectory(path+"/"+aname, adir, nil, idmap, ignore)...)
} else {
// directory is in both trees; descend
if attributes := compareFileInfo(adir.checkFileInfo, bdir.checkFileInfo, idmap, ignore); attributes != "" {
diff = append(diff, path+"/"+aname+"("+attributes+")")
}
diff = append(diff, compareCheckSubdirectory(path+"/"+aname, adir, bdir, idmap, ignore)...)
}
}
for bname, bdir := range b.directory {
if _, present := a.directory[bname]; !present {
// directory added
diff = append(diff, "+"+path+"/"+bname+"/")
diff = append(diff, compareCheckSubdirectory(path+"/"+bname, nil, bdir, idmap, ignore)...)
}
}
for aname, afile := range a.file {
if bfile, present := b.file[aname]; !present {
// non-directory removed or replaced
diff = append(diff, "-"+path+"/"+aname)
} else {
// item is in both trees; compare
if attributes := compareFileInfo(afile, bfile, idmap, ignore); attributes != "" {
diff = append(diff, path+"/"+aname+"("+attributes+")")
}
}
}
for bname := range b.file {
filetype, present := a.file[bname]
if !present {
// non-directory added or replaced with something else
diff = append(diff, "+"+path+"/"+bname)
continue
}
if attributes := compareFileInfo(filetype, b.file[bname], idmap, ignore); attributes != "" {
// non-directory replaced with non-directory
diff = append(diff, "+"+path+"/"+bname+"("+attributes+")")
}
}
return diff
}
// compareCheckDirectory walks two directory trees and returns a sorted list of differences
func compareCheckDirectory(a, b *checkDirectory, idmap *idtools.IDMappings, ignore checkIgnore) []string {
diff := compareCheckSubdirectory("", a, b, idmap, ignore)
sort.Slice(diff, func(i, j int) bool {
if strings.Compare(diff[i][1:], diff[j][1:]) < 0 {
return true
}
if diff[i][0] == '-' {
return true
}
return false
})
return diff
}