libmage: Exists: catch corrupted images

While various execution paths in libimage already handle corrupted
images, `(*Runtime).Exists()` did not and would list an image to exist
in the storage even if it is corrupted.

Some corruptions can only be detected when accessing the individual
data.  A reliable way of accessing such data is to inspect an image.
Hence, an image will only be listed to exist if a) it has been found
and b) can be inspected.  If the inspection fails, the image will be
reported to not exists but without an error.  That allows for users
of libimage to properly recover and repull.

Further, add a new unit tests that forces a data corruption and
gradually recovers from it.

Podman will now behave as follows:
```
$ ./bin/podman run -d --rm nginx ls
ERRO[0000] Image nginx exists in local storage but may be corrupted: layer not known
ERRO[0000] Looking up nginx in local storage: layer not known
Resolved "nginx" as an alias (/home/vrothberg/.cache/containers/short-name-aliases.conf)
Trying to pull docker.io/library/nginx:latest...
Getting image source signatures
Copying blob 596b1d696923 skipped: already exists
Copying blob 30afc0b18f67 skipped: already exists
Copying blob febe5bd23e98 skipped: already exists
Copying blob 69692152171a skipped: already exists
Copying blob 8283eee92e2f skipped: already exists
Copying blob 351ad75a6cfa done
Copying config d1a364dc54 done
Writing manifest to image destination
Storing signatures
56b65883c3c32b67277bcc173bd9f26c27cbbdbc6d3aacf6c552be796eb7a337
```

Signed-off-by: Valentin Rothberg <rothberg@redhat.com>
This commit is contained in:
Valentin Rothberg 2021-06-10 10:34:03 +02:00
parent 6ea4d5c7f8
commit 7f038138c3
2 changed files with 85 additions and 3 deletions

View File

@ -0,0 +1,77 @@
package libimage
import (
"context"
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/containers/common/pkg/config"
"github.com/containers/storage"
"github.com/containers/storage/pkg/ioutils"
"github.com/stretchr/testify/require"
)
func TestCorruptedImage(t *testing.T) {
// Regression tests for https://bugzilla.redhat.com/show_bug.cgi?id=1966872.
runtime, cleanup := testNewRuntime(t)
defer cleanup()
ctx := context.Background()
pullOptions := &PullOptions{}
pullOptions.Writer = os.Stdout
imageName := "quay.io/libpod/alpine_nginx:latest"
pulledImages, err := runtime.Pull(ctx, imageName, config.PullPolicyAlways, pullOptions)
require.NoError(t, err)
require.Len(t, pulledImages, 1)
image := pulledImages[0]
// Inpsecting a healthy image should work.
_, err = image.Inspect(ctx, false)
require.NoError(t, err, "inspecting healthy image should work")
exists, err := runtime.Exists(imageName)
require.NoError(t, err, "healthy image exists")
require.True(t, exists, "healthy image exists")
// Now remove one layer from the layers.json index in the storage. The
// image will still be listed in the container storage but attempting
// to use it will yield "layer not known" errors.
indexPath := filepath.Join(runtime.store.GraphRoot(), "vfs-layers/layers.json")
data, err := ioutil.ReadFile(indexPath)
require.NoError(t, err, "loading layers.json")
layers := []*storage.Layer{}
err = json.Unmarshal(data, &layers)
require.NoError(t, err, "unmarshaling layers.json")
require.LessOrEqual(t, 1, len(layers), "at least one layer must be present")
// Now write back the layers without the first layer!
data, err = json.Marshal(layers[1:])
require.NoError(t, err, "unmarshaling layers.json")
err = ioutils.AtomicWriteFile(indexPath, data, 0600) // nolint
require.NoError(t, err, "writing back layers.json")
image.reload() // clear the cached data
// Now inspecting the image must fail!
_, err = image.Inspect(ctx, false)
require.Error(t, err, "inspecting corrupted image should fail")
exists, err = runtime.Exists(imageName)
require.NoError(t, err, "corrupted image exists should not fail")
require.False(t, exists, "corrupted image should not be marked to exist")
// Now make sure that pull will detect the corrupted image and repulls
// if needed which will repair the data corruption.
pulledImages, err = runtime.Pull(ctx, imageName, config.PullPolicyNewer, pullOptions)
require.NoError(t, err)
require.Len(t, pulledImages, 1)
image = pulledImages[0]
// Inpsecting a repaired image should work.
_, err = image.Inspect(ctx, false)
require.NoError(t, err, "inspecting repaired image should work")
}

View File

@ -132,13 +132,18 @@ func (r *Runtime) storageToImage(storageImage *storage.Image, ref types.ImageRef
} }
// Exists returns true if the specicifed image exists in the local containers // Exists returns true if the specicifed image exists in the local containers
// storage. // storage. Note that it may return false if an image corrupted.
func (r *Runtime) Exists(name string) (bool, error) { func (r *Runtime) Exists(name string) (bool, error) {
image, _, err := r.LookupImage(name, &LookupImageOptions{IgnorePlatform: true}) image, _, err := r.LookupImage(name, &LookupImageOptions{IgnorePlatform: true})
if err != nil && errors.Cause(err) != storage.ErrImageUnknown { if image == nil || err != nil && errors.Cause(err) != storage.ErrImageUnknown {
return false, err return false, err
} }
return image != nil, nil // Inspect the image to make sure if it's corrupted or not.
if _, err := image.Inspect(context.Background(), false); err != nil {
logrus.Errorf("Image %s exists in local storage but may be corrupted: %v", name, err)
return false, nil
}
return true, nil
} }
// LookupImageOptions allow for customizing local image lookups. // LookupImageOptions allow for customizing local image lookups.