diff --git a/oci/layout/oci_delete.go b/oci/layout/oci_delete.go new file mode 100644 index 0000000000..be1f3c84d4 --- /dev/null +++ b/oci/layout/oci_delete.go @@ -0,0 +1,244 @@ +package layout + +import ( + "bytes" + "context" + "encoding/json" + "io/fs" + "os" + "path/filepath" + + "github.com/containers/image/v5/internal/set" + "github.com/containers/image/v5/types" + digest "github.com/opencontainers/go-digest" + imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/sirupsen/logrus" +) + +// DeleteImage deletes the named image from the directory, if supported. +func (ref ociReference) DeleteImage(ctx context.Context, sys *types.SystemContext) error { + // Scan all the manifests in the directory: + // ... collect the one that matches with the received ref + // ... and store all the blobs used in all other images + var imageDescriptorWrapper *descriptorWrapper + blobsUsedByOtherImages := set.New[digest.Digest]() + allDescriptors, err := ref.getAllImageDescriptorsInDirectory() + if err != nil { + return err + } + + if ref.image == "" { + if len(allDescriptors) == 1 { + imageDescriptorWrapper = &allDescriptors[0] + } else { + return ErrMoreThanOneImage + } + } else { + for _, v := range allDescriptors { + if v.descriptor.Annotations[imgspecv1.AnnotationRefName] == ref.image { + tmpDescriptionWrapper := v + imageDescriptorWrapper = &tmpDescriptionWrapper + } else { + otherImageManifest, err := ref.getManifest(v.descriptor) + if err != nil { + return err + } + blobsUsedByOtherImages.Add(otherImageManifest.Config.Digest) + for _, layer := range otherImageManifest.Layers { + blobsUsedByOtherImages.Add(layer.Digest) + } + } + } + } + + if ref.image != "" && imageDescriptorWrapper == nil { + return ImageNotFoundError{ref} + } + + manifest, err := ref.getManifest(imageDescriptorWrapper.descriptor) + if err != nil { + return err + } + + // Delete all blobs used by this image only + blobsToDelete := set.New[digest.Digest]() + for _, descriptor := range append(manifest.Layers, manifest.Config, *imageDescriptorWrapper.descriptor) { + if !blobsUsedByOtherImages.Contains(descriptor.Digest) { + blobsToDelete.Add(descriptor.Digest) + } else { + logrus.Debug("Blob ", descriptor.Digest.Hex(), " is used by another image, leaving it") + } + } + for _, digest := range blobsToDelete.Values() { + //TODO Check if there is shared blob path? + blobPath, err := ref.blobPath(digest, "") + if err != nil { + return err + } + logrus.Debug("Deleting blob ", digest.Hex()) + err = os.Remove(blobPath) + if err != nil && !os.IsNotExist(err) { + return err + } + } + + // This holds the step to be done on the current index, as we walk back bottom up + step := indexUpdateStep{"delete", imageDescriptorWrapper.descriptor.Digest, nil} + + for i := len(imageDescriptorWrapper.indexChain) - 1; i >= 0; i-- { + indexPath := imageDescriptorWrapper.indexChain[i] + index, err := parseIndex(indexPath) + if err != nil { + return err + } + // Fill new index with existing manifests except the one we are removing + newManifests := make([]imgspecv1.Descriptor, 0, len(index.Manifests)) + for _, v := range index.Manifests { + if v.Digest == step.digest { + switch step.action { + case "delete": + continue + case "update": + newDescriptor := v + newDescriptor.Digest = *step.newDigest + newManifests = append(newManifests, newDescriptor) + } + } else { + newManifests = append(newManifests, v) + } + } + index.Manifests = newManifests + + // New index is ready, it has to be saved to disk now + // ... if it is the root index, it's easy, just overwrite it + if indexPath == ref.indexPath() { + return saveJSON(ref.indexPath(), index) + } else { + indexDigest, err := digest.Parse("sha256:" + filepath.Base(indexPath)) + if err != nil { + return err + } + // In a nested index, if the new index is empty it has to be remove, + // otherwise update the parent index with the new hash + if len(index.Manifests) == 0 { + step = indexUpdateStep{"delete", indexDigest, nil} + } else { + // Save the new file + buffer := new(bytes.Buffer) + err = json.NewEncoder(buffer).Encode(index) + if err != nil { + return err + } + indexNewDigest := digest.Canonical.FromBytes(buffer.Bytes()) + indexNewPath, err := ref.blobPath(indexNewDigest, "") + if err != nil { + return err + } + err = saveJSON(indexNewPath, index) + if err != nil { + return err + } + step = indexUpdateStep{"update", indexDigest, &indexNewDigest} + } + // Delete the current index if it is not reference anywhere else; + // it is dangling by now as it'll either be empty or have a new hash + if !blobsUsedByOtherImages.Contains(indexDigest) { + err = os.Remove(indexPath) + if err != nil { + return err + } + } + } + } + + return nil +} + +func saveJSON(path string, content any) error { + // If the file already exists, get its mode to preserve it + var mode fs.FileMode + existingfi, err := os.Stat(path) + if err != nil { + if !os.IsNotExist(err) { + return err + } else { // File does not exist, use default mode + mode = 0644 + } + } else { + mode = existingfi.Mode() + } + + // Then write the file + file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode) + if err != nil { + return err + } + defer file.Close() + + return json.NewEncoder(file).Encode(content) +} + +// Stores an (image) descriptor along with the index it was found in and its parents if any +// this allows the update of the index when an image is located in a nested (++) index +type descriptorWrapper struct { + descriptor *imgspecv1.Descriptor + indexChain []string //in order of appearence, the first is always be index.json and the nested indexes, last one being the one where the descriptor was found in +} + +// This will return all the descriptors of all the images found in the directory +// It starts at the index.json and then walks all nested indexes +// Each image descriptor is returned along with the index it was found, as well as it parents if it is a nested index +func (ref ociReference) getAllImageDescriptorsInDirectory() ([]descriptorWrapper, error) { + descriptors := make([]descriptorWrapper, 0) + var getImageDescriptorsFromIndex func(indexChain []string) error + getImageDescriptorsFromIndex = func(indexChain []string) error { + index, err := parseIndex(indexChain[len(indexChain)-1]) // last item in the index is always the index in which whe are currently working + if err != nil { + return err + } + + for _, manifestDescriptor := range index.Manifests { + switch manifestDescriptor.MediaType { + case imgspecv1.MediaTypeImageManifest: + tmpManifestDescriptor := manifestDescriptor + wrapper := descriptorWrapper{&tmpManifestDescriptor, indexChain} + descriptors = append(descriptors, wrapper) + case imgspecv1.MediaTypeImageIndex: + nestedIndexBlobPath, err := ref.blobPath(manifestDescriptor.Digest, "") + if err != nil { + return err + } + // recursively get manifests from this nested index + err = getImageDescriptorsFromIndex(append(indexChain, nestedIndexBlobPath)) + if err != nil { + return err + } + } + } + return nil + } + + err := getImageDescriptorsFromIndex([]string{ref.indexPath()}) //Start the walk at the root (index.json) + return descriptors, err +} + +func (ref ociReference) getManifest(descriptor *imgspecv1.Descriptor) (*imgspecv1.Manifest, error) { + //TODO Check if there is shared blob path? + manifestPath, err := ref.blobPath(descriptor.Digest, "") + if err != nil { + return nil, err + } + + manifest, err := parseJSON[imgspecv1.Manifest](manifestPath) + if err != nil { + return nil, err + } + + return manifest, nil +} + +type indexUpdateStep struct { + action string + digest digest.Digest + newDigest *digest.Digest +} diff --git a/oci/layout/oci_delete_test.go b/oci/layout/oci_delete_test.go new file mode 100644 index 0000000000..8cdf5cc9b5 --- /dev/null +++ b/oci/layout/oci_delete_test.go @@ -0,0 +1,186 @@ +package layout + +import ( + "context" + "fmt" + "os" + "path/filepath" + "testing" + + imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" + cp "github.com/otiai10/copy" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestReferenceDeleteImage(t *testing.T) { + tmpDir := loadFixture(t, "delete_image") + + ref, err := NewReference(tmpDir, "latest") + require.NoError(t, err) + + err = ref.DeleteImage(context.Background(), nil) + require.NoError(t, err) + + // Check that all blobs were deleted + blobsDir := filepath.Join(tmpDir, "blobs") + files, err := os.ReadDir(filepath.Join(blobsDir, "sha256")) + require.NoError(t, err) + require.Empty(t, files) + + // Check that the index is empty as there is only one image in the fixture + ociRef, ok := ref.(ociReference) + require.True(t, ok) + index, err := ociRef.getIndex() + require.NoError(t, err) + require.Equal(t, 0, len(index.Manifests)) +} + +func TestReferenceDeleteImage_emptyImageName(t *testing.T) { + tmpDir := loadFixture(t, "delete_image") + + ref, err := NewReference(tmpDir, "") + require.NoError(t, err) + + err = ref.DeleteImage(context.Background(), nil) + require.NoError(t, err) + + // Check that all blobs were deleted + blobsDir := filepath.Join(tmpDir, "blobs") + files, err := os.ReadDir(filepath.Join(blobsDir, "sha256")) + require.NoError(t, err) + require.Empty(t, files) + + // Check that the index is empty as there is only one image in the fixture + ociRef, ok := ref.(ociReference) + require.True(t, ok) + index, err := ociRef.getIndex() + require.NoError(t, err) + require.Equal(t, 0, len(index.Manifests)) +} + +func TestReferenceDeleteImage_imageDoesNotExist(t *testing.T) { + tmpDir := loadFixture(t, "delete_image") + + ref, err := NewReference(tmpDir, "does-not-exist") + assert.NoError(t, err) + + err = ref.DeleteImage(context.Background(), nil) + assert.Error(t, err) +} + +func TestReferenceDeleteImage_moreThanOneImageInIndex(t *testing.T) { + tmpDir := loadFixture(t, "delete_image_multipleimages") + + ref, err := NewReference(tmpDir, "3.2") + require.NoError(t, err) + + err = ref.DeleteImage(context.Background(), nil) + require.NoError(t, err) + + // Check that the relevant blobs were deleted/preservend + blobsDir := filepath.Join(tmpDir, "blobs") + blobDoesNotExist(t, blobsDir, "sha256:9a48d58d496b700f364686fbfbb2141ff5f0f25b033078a4c11fe597770b6fab") // menifest of the deleted image + blobDoesNotExist(t, blobsDir, "sha256:8f891520c22dc085f86a1a9aef2e1165e63e7465ae2112df6bd1d7a115a12f8e") // config of the deleted image + blobDoesNotExist(t, blobsDir, "sha256:d107df792639f1ee2fc4555597cb0eec8978b07e45a68f782965fd00a8964545") // layer of the deleted image + blobExists(t, blobsDir, "sha256:f082a2f88d9405f9d583e5038c76290d10dbefdb9b2137301c1e867f6f43cff6") // manifest of the other image present in the index + blobExists(t, blobsDir, "sha256:a527179158cd5cebc11c152b8637b47ce96c838ba2aa0de66d14f45cedc11423") // config of the other image present in the index + blobExists(t, blobsDir, "sha256:bc584603ae5ca55d701f5134a0e5699056536885580ee929945bcbfeaf2633e6") // layer of the other image present in the index + + // Check that the index doesn't contain the reference anymore + ociRef, ok := ref.(ociReference) + require.True(t, ok) + descriptors, err := ociRef.getAllImageDescriptorsInDirectory() + require.NoError(t, err) + otherImageStillPresent := false //This will track that other images are still there + for _, v := range descriptors { + switch v.descriptor.Annotations[imgspecv1.AnnotationRefName] { + case ociRef.image: + assert.Fail(t, "image still present in the index after deletion") + case "3.10.2": + otherImageStillPresent = true + } + } + require.True(t, otherImageStillPresent) +} + +func TestReferenceDeleteImage_emptyImageNameButMoreThanOneImageInIndex(t *testing.T) { + tmpDir := loadFixture(t, "delete_image_multipleimages") + + ref, err := NewReference(tmpDir, "") + require.NoError(t, err) + + err = ref.DeleteImage(context.Background(), nil) + require.Error(t, err) +} + +func TestReferenceDeleteImage_someBlobsAreUsedByOtherImages(t *testing.T) { + tmpDir := loadFixture(t, "delete_image_sharedblobs") + + ref, err := NewReference(tmpDir, "3.2") + require.NoError(t, err) + + err = ref.DeleteImage(context.Background(), nil) + require.NoError(t, err) + + // Check that the relevant blobs were deleted/preserved + blobsDir := filepath.Join(tmpDir, "blobs") + blobDoesNotExist(t, blobsDir, "sha256:2363edaccd5115dad0462eac535496a0b7b661311d1fb8ed7a1f51368bfa9f3a") // manifest for the image + blobExists(t, blobsDir, "sha256:8f891520c22dc085f86a1a9aef2e1165e63e7465ae2112df6bd1d7a115a12f8e") // configuration, used by another image too + blobExists(t, blobsDir, "sha256:d107df792639f1ee2fc4555597cb0eec8978b07e45a68f782965fd00a8964545") // layer, used by another image too + blobDoesNotExist(t, blobsDir, "sha256:49b6418afb4ee08ba3956e4c344034c89a39ef1a451a55b44926ad9ee77e036b") // layer used by that image only + + // Check that the index doesn't contain the reference anymore + ociRef, ok := ref.(ociReference) + require.True(t, ok) + descriptors, err := ociRef.getAllImageDescriptorsInDirectory() + require.NoError(t, err) + otherImagesStillPresent := make([]bool, 0, 2) //This will track that other images are still there + for _, v := range descriptors { + switch v.descriptor.Annotations[imgspecv1.AnnotationRefName] { + case ociRef.image: + assert.Fail(t, "image still present in the index after deletion") + case "3.10.2": + otherImagesStillPresent = append(otherImagesStillPresent, true) + case "latest": + otherImagesStillPresent = append(otherImagesStillPresent, true) + } + } + assert.Equal(t, []bool{true, true}, otherImagesStillPresent) +} + +func TestReferenceDeleteImage_inNestedIndex(t *testing.T) { + tmpDir := loadFixture(t, "delete_image_nestedindex") + + ref, err := NewReference(tmpDir, "latest") + require.NoError(t, err) + + err = ref.DeleteImage(context.Background(), nil) + require.NoError(t, err) + + // Check that all relevant blobs were deleted/preserved + blobsDir := filepath.Join(tmpDir, "blobs") + blobDoesNotExist(t, blobsDir, "sha256:4a6da698b869046086d0e6ba846f8b931cb33bbaa5c68025b4fd55f67a4f0513") // manifest for the image + blobDoesNotExist(t, blobsDir, "sha256:a527179158cd5cebc11c152b8637b47ce96c838ba2aa0de66d14f45cedc11423") // configuration for the image + blobDoesNotExist(t, blobsDir, "sha256:0c8b263642b51b5c1dc40fe402ae2e97119c6007b6e52146419985ec1f0092dc") // layer used by that image only + blobExists(t, blobsDir, "sha256:d107df792639f1ee2fc4555597cb0eec8978b07e45a68f782965fd00a8964545") // layer used by another image in the index(es) + + // Check that a few new blobs have been created after index deletion/update + blobDoesNotExist(t, blobsDir, "sha256:fbe294d1b627d6ee3c119d558dad8b1c4542cbc51c49ec45dd638921bc5921d0") // nested index 2 that contained the image and only that image + blobDoesNotExist(t, blobsDir, "sha256:b2ff1c27b718b90910711aeda5e02ebbf4440659edd589cc458b3039ea91b35f") // nested index 1, should have been renamed - see next line + blobExists(t, blobsDir, "sha256:13e9f5dde0af5d4303ef0e69d847bc14db6c86a7df616831e126821daf532982") // new sha of the nested index + + // Check that the index has been update with the new nestedindex's sha + ociRef, ok := ref.(ociReference) + require.True(t, ok) + index, err := ociRef.getIndex() + require.NoError(t, err) + assert.Equal(t, 1, len(index.Manifests)) +} + +func loadFixture(t *testing.T, fixtureName string) string { + tmpDir := t.TempDir() + err := cp.Copy(fmt.Sprintf("fixtures/%v/", fixtureName), tmpDir) + require.NoError(t, err) + return tmpDir +} diff --git a/oci/layout/oci_transport.go b/oci/layout/oci_transport.go index 378a51f1fc..784f16fc0f 100644 --- a/oci/layout/oci_transport.go +++ b/oci/layout/oci_transport.go @@ -1,12 +1,10 @@ package layout import ( - "bytes" "context" "encoding/json" "errors" "fmt" - "io/fs" "os" "path/filepath" "strings" @@ -14,13 +12,11 @@ import ( "github.com/containers/image/v5/directory/explicitfilepath" "github.com/containers/image/v5/docker/reference" "github.com/containers/image/v5/internal/image" - "github.com/containers/image/v5/internal/set" "github.com/containers/image/v5/oci/internal" "github.com/containers/image/v5/transports" "github.com/containers/image/v5/types" "github.com/opencontainers/go-digest" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" - "github.com/sirupsen/logrus" ) func init() { @@ -167,6 +163,10 @@ func (ref ociReference) getIndex() (*imgspecv1.Index, error) { return parseIndex(ref.indexPath()) } +func parseIndex(path string) (*imgspecv1.Index, error) { + return parseJSON[imgspecv1.Index](path) +} + func parseJSON[T any](path string) (*T, error) { content, err := os.Open(path) if err != nil { @@ -181,10 +181,6 @@ func parseJSON[T any](path string) (*T, error) { return obj, nil } -func parseIndex(path string) (*imgspecv1.Index, error) { - return parseJSON[imgspecv1.Index](path) -} - func (ref ociReference) getManifestDescriptor() (imgspecv1.Descriptor, error) { index, err := ref.getIndex() if err != nil { @@ -216,65 +212,6 @@ func (ref ociReference) getManifestDescriptor() (imgspecv1.Descriptor, error) { return imgspecv1.Descriptor{}, ImageNotFoundError{ref} } -// Stores an (image) descriptor along with the index it was found in and its parents if any -// this allows the update of the index when an image is located in a nested (++) index -type descriptorWrapper struct { - descriptor *imgspecv1.Descriptor - indexChain []string //in order of appearence, the first is always be index.json and the nested indexes, last one being the one where the descriptor was found in -} - -// This will return all the descriptors of all the images found in the directory -// It starts at the index.json and then walks all nested indexes -// Each image descriptor is returned along with the index it was found, as well as it parents if it is a nested index -func (ref ociReference) getAllImageDescriptorsInDirectory() ([]descriptorWrapper, error) { - descriptors := make([]descriptorWrapper, 0) - var getImageDescriptorsFromIndex func(indexChain []string) error - getImageDescriptorsFromIndex = func(indexChain []string) error { - index, err := parseIndex(indexChain[len(indexChain)-1]) // last item in the index is always the index in which whe are currently working - if err != nil { - return err - } - - for _, manifestDescriptor := range index.Manifests { - switch manifestDescriptor.MediaType { - case imgspecv1.MediaTypeImageManifest: - tmpManifestDescriptor := manifestDescriptor - wrapper := descriptorWrapper{&tmpManifestDescriptor, indexChain} - descriptors = append(descriptors, wrapper) - case imgspecv1.MediaTypeImageIndex: - nestedIndexBlobPath, err := ref.blobPath(manifestDescriptor.Digest, "") - if err != nil { - return err - } - // recursively get manifests from this nested index - err = getImageDescriptorsFromIndex(append(indexChain, nestedIndexBlobPath)) - if err != nil { - return err - } - } - } - return nil - } - - err := getImageDescriptorsFromIndex([]string{ref.indexPath()}) //Start the walk at the root (index.json) - return descriptors, err -} - -func (ref ociReference) getManifest(descriptor *imgspecv1.Descriptor) (*imgspecv1.Manifest, error) { - //TODO Check if there is shared blob path? - manifestPath, err := ref.blobPath(descriptor.Digest, "") - if err != nil { - return nil, err - } - - manifest, err := parseJSON[imgspecv1.Manifest](manifestPath) - if err != nil { - return nil, err - } - - return manifest, nil -} - // LoadManifestDescriptor loads the manifest descriptor to be used to retrieve the image name // when pulling an image func LoadManifestDescriptor(imgRef types.ImageReference) (imgspecv1.Descriptor, error) { @@ -297,175 +234,6 @@ func (ref ociReference) NewImageDestination(ctx context.Context, sys *types.Syst return newImageDestination(sys, ref) } -type indexUpdateStep struct { - action string - digest digest.Digest - newDigest *digest.Digest -} - -// DeleteImage deletes the named image from the directory, if supported. -func (ref ociReference) DeleteImage(ctx context.Context, sys *types.SystemContext) error { - // Scan all the manifests in the directory: - // ... collect the one that matches with the received ref - // ... and store all the blobs used in all other images - var imageDescriptorWrapper *descriptorWrapper - blobsUsedByOtherImages := set.New[digest.Digest]() - allDescriptors, err := ref.getAllImageDescriptorsInDirectory() - if err != nil { - return err - } - - if ref.image == "" { - if len(allDescriptors) == 1 { - imageDescriptorWrapper = &allDescriptors[0] - } else { - return ErrMoreThanOneImage - } - } else { - for _, v := range allDescriptors { - if v.descriptor.Annotations[imgspecv1.AnnotationRefName] == ref.image { - tmpDescriptionWrapper := v - imageDescriptorWrapper = &tmpDescriptionWrapper - } else { - otherImageManifest, err := ref.getManifest(v.descriptor) - if err != nil { - return err - } - blobsUsedByOtherImages.Add(otherImageManifest.Config.Digest) - for _, layer := range otherImageManifest.Layers { - blobsUsedByOtherImages.Add(layer.Digest) - } - } - } - } - - if ref.image != "" && imageDescriptorWrapper == nil { - return ImageNotFoundError{ref} - } - - manifest, err := ref.getManifest(imageDescriptorWrapper.descriptor) - if err != nil { - return err - } - - // Delete all blobs used by this image only - blobsToDelete := set.New[digest.Digest]() - for _, descriptor := range append(manifest.Layers, manifest.Config, *imageDescriptorWrapper.descriptor) { - if !blobsUsedByOtherImages.Contains(descriptor.Digest) { - blobsToDelete.Add(descriptor.Digest) - } else { - logrus.Debug("Blob ", descriptor.Digest.Hex(), " is used by another image, leaving it") - } - } - for _, digest := range blobsToDelete.Values() { - //TODO Check if there is shared blob path? - blobPath, err := ref.blobPath(digest, "") - if err != nil { - return err - } - logrus.Debug("Deleting blob ", digest.Hex()) - err = os.Remove(blobPath) - if err != nil && !os.IsNotExist(err) { - return err - } - } - - // This holds the step to be done on the current index, as we walk back bottom up - step := indexUpdateStep{"delete", imageDescriptorWrapper.descriptor.Digest, nil} - - for i := len(imageDescriptorWrapper.indexChain) - 1; i >= 0; i-- { - indexPath := imageDescriptorWrapper.indexChain[i] - index, err := parseIndex(indexPath) - if err != nil { - return err - } - // Fill new index with existing manifests except the one we are removing - newManifests := make([]imgspecv1.Descriptor, 0, len(index.Manifests)) - for _, v := range index.Manifests { - if v.Digest == step.digest { - switch step.action { - case "delete": - continue - case "update": - newDescriptor := v - newDescriptor.Digest = *step.newDigest - newManifests = append(newManifests, newDescriptor) - } - } else { - newManifests = append(newManifests, v) - } - } - index.Manifests = newManifests - - // New index is ready, it has to be saved to disk now - // ... if it is the root index, it's easy, just overwrite it - if indexPath == ref.indexPath() { - return saveJSON(ref.indexPath(), index) - } else { - indexDigest, err := digest.Parse("sha256:" + filepath.Base(indexPath)) - if err != nil { - return err - } - // In a nested index, if the new index is empty it has to be remove, - // otherwise update the parent index with the new hash - if len(index.Manifests) == 0 { - step = indexUpdateStep{"delete", indexDigest, nil} - } else { - // Save the new file - buffer := new(bytes.Buffer) - err = json.NewEncoder(buffer).Encode(index) - if err != nil { - return err - } - indexNewDigest := digest.Canonical.FromBytes(buffer.Bytes()) - indexNewPath, err := ref.blobPath(indexNewDigest, "") - if err != nil { - return err - } - err = saveJSON(indexNewPath, index) - if err != nil { - return err - } - step = indexUpdateStep{"update", indexDigest, &indexNewDigest} - } - // Delete the current index if it is not reference anywhere else; - // it is dangling by now as it'll either be empty or have a new hash - if !blobsUsedByOtherImages.Contains(indexDigest) { - err = os.Remove(indexPath) - if err != nil { - return err - } - } - } - } - - return nil -} - -func saveJSON(path string, content any) error { - // If the file already exists, get its mode to preserve it - var mode fs.FileMode - existingfi, err := os.Stat(path) - if err != nil { - if !os.IsNotExist(err) { - return err - } else { // File does not exist, use default mode - mode = 0644 - } - } else { - mode = existingfi.Mode() - } - - // Then write the file - file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode) - if err != nil { - return err - } - defer file.Close() - - return json.NewEncoder(file).Encode(content) -} - // ociLayoutPath returns a path for the oci-layout within a directory using OCI conventions. func (ref ociReference) ociLayoutPath() string { return filepath.Join(ref.dir, "oci-layout") diff --git a/oci/layout/oci_transport_test.go b/oci/layout/oci_transport_test.go index 66d5a660f1..248bb10ec9 100644 --- a/oci/layout/oci_transport_test.go +++ b/oci/layout/oci_transport_test.go @@ -2,7 +2,6 @@ package layout import ( "context" - "fmt" "os" "path/filepath" "testing" @@ -11,7 +10,6 @@ import ( "github.com/containers/image/v5/types" "github.com/opencontainers/go-digest" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" - cp "github.com/otiai10/copy" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -322,178 +320,6 @@ func TestReferenceNewImageDestination(t *testing.T) { defer dest.Close() } -func loadFixture(t *testing.T, fixtureName string) string { - tmpDir := t.TempDir() - err := cp.Copy(fmt.Sprintf("fixtures/%v/", fixtureName), tmpDir) - require.NoError(t, err) - return tmpDir -} - -func TestReferenceDeleteImage(t *testing.T) { - tmpDir := loadFixture(t, "delete_image") - - ref, err := NewReference(tmpDir, "latest") - require.NoError(t, err) - - err = ref.DeleteImage(context.Background(), nil) - require.NoError(t, err) - - // Check that all blobs were deleted - blobsDir := filepath.Join(tmpDir, "blobs") - files, err := os.ReadDir(filepath.Join(blobsDir, "sha256")) - require.NoError(t, err) - require.Empty(t, files) - - // Check that the index is empty as there is only one image in the fixture - ociRef, ok := ref.(ociReference) - require.True(t, ok) - index, err := ociRef.getIndex() - require.NoError(t, err) - require.Equal(t, 0, len(index.Manifests)) -} - -func TestReferenceDeleteImage_emptyImageName(t *testing.T) { - tmpDir := loadFixture(t, "delete_image") - - ref, err := NewReference(tmpDir, "") - require.NoError(t, err) - - err = ref.DeleteImage(context.Background(), nil) - require.NoError(t, err) - - // Check that all blobs were deleted - blobsDir := filepath.Join(tmpDir, "blobs") - files, err := os.ReadDir(filepath.Join(blobsDir, "sha256")) - require.NoError(t, err) - require.Empty(t, files) - - // Check that the index is empty as there is only one image in the fixture - ociRef, ok := ref.(ociReference) - require.True(t, ok) - index, err := ociRef.getIndex() - require.NoError(t, err) - require.Equal(t, 0, len(index.Manifests)) -} - -func TestReferenceDeleteImage_imageDoesNotExist(t *testing.T) { - tmpDir := loadFixture(t, "delete_image") - - ref, err := NewReference(tmpDir, "does-not-exist") - assert.NoError(t, err) - - err = ref.DeleteImage(context.Background(), nil) - assert.Error(t, err) -} - -func TestReferenceDeleteImage_moreThanOneImageInIndex(t *testing.T) { - tmpDir := loadFixture(t, "delete_image_multipleimages") - - ref, err := NewReference(tmpDir, "3.2") - require.NoError(t, err) - - err = ref.DeleteImage(context.Background(), nil) - require.NoError(t, err) - - // Check that the relevant blobs were deleted/preservend - blobsDir := filepath.Join(tmpDir, "blobs") - blobDoesNotExist(t, blobsDir, "sha256:9a48d58d496b700f364686fbfbb2141ff5f0f25b033078a4c11fe597770b6fab") // menifest of the deleted image - blobDoesNotExist(t, blobsDir, "sha256:8f891520c22dc085f86a1a9aef2e1165e63e7465ae2112df6bd1d7a115a12f8e") // config of the deleted image - blobDoesNotExist(t, blobsDir, "sha256:d107df792639f1ee2fc4555597cb0eec8978b07e45a68f782965fd00a8964545") // layer of the deleted image - blobExists(t, blobsDir, "sha256:f082a2f88d9405f9d583e5038c76290d10dbefdb9b2137301c1e867f6f43cff6") // manifest of the other image present in the index - blobExists(t, blobsDir, "sha256:a527179158cd5cebc11c152b8637b47ce96c838ba2aa0de66d14f45cedc11423") // config of the other image present in the index - blobExists(t, blobsDir, "sha256:bc584603ae5ca55d701f5134a0e5699056536885580ee929945bcbfeaf2633e6") // layer of the other image present in the index - - // Check that the index doesn't contain the reference anymore - ociRef, ok := ref.(ociReference) - require.True(t, ok) - descriptors, err := ociRef.getAllImageDescriptorsInDirectory() - require.NoError(t, err) - otherImageStillPresent := false //This will track that other images are still there - for _, v := range descriptors { - switch v.descriptor.Annotations[imgspecv1.AnnotationRefName] { - case ociRef.image: - assert.Fail(t, "image still present in the index after deletion") - case "3.10.2": - otherImageStillPresent = true - } - } - require.True(t, otherImageStillPresent) -} - -func TestReferenceDeleteImage_emptyImageNameButMoreThanOneImageInIndex(t *testing.T) { - tmpDir := loadFixture(t, "delete_image_multipleimages") - - ref, err := NewReference(tmpDir, "") - require.NoError(t, err) - - err = ref.DeleteImage(context.Background(), nil) - require.Error(t, err) -} - -func TestReferenceDeleteImage_someBlobsAreUsedByOtherImages(t *testing.T) { - tmpDir := loadFixture(t, "delete_image_sharedblobs") - - ref, err := NewReference(tmpDir, "3.2") - require.NoError(t, err) - - err = ref.DeleteImage(context.Background(), nil) - require.NoError(t, err) - - // Check that the relevant blobs were deleted/preserved - blobsDir := filepath.Join(tmpDir, "blobs") - blobDoesNotExist(t, blobsDir, "sha256:2363edaccd5115dad0462eac535496a0b7b661311d1fb8ed7a1f51368bfa9f3a") // manifest for the image - blobExists(t, blobsDir, "sha256:8f891520c22dc085f86a1a9aef2e1165e63e7465ae2112df6bd1d7a115a12f8e") // configuration, used by another image too - blobExists(t, blobsDir, "sha256:d107df792639f1ee2fc4555597cb0eec8978b07e45a68f782965fd00a8964545") // layer, used by another image too - blobDoesNotExist(t, blobsDir, "sha256:49b6418afb4ee08ba3956e4c344034c89a39ef1a451a55b44926ad9ee77e036b") // layer used by that image only - - // Check that the index doesn't contain the reference anymore - ociRef, ok := ref.(ociReference) - require.True(t, ok) - descriptors, err := ociRef.getAllImageDescriptorsInDirectory() - require.NoError(t, err) - otherImagesStillPresent := make([]bool, 0, 2) //This will track that other images are still there - for _, v := range descriptors { - switch v.descriptor.Annotations[imgspecv1.AnnotationRefName] { - case ociRef.image: - assert.Fail(t, "image still present in the index after deletion") - case "3.10.2": - otherImagesStillPresent = append(otherImagesStillPresent, true) - case "latest": - otherImagesStillPresent = append(otherImagesStillPresent, true) - } - } - assert.Equal(t, []bool{true, true}, otherImagesStillPresent) -} - -func TestReferenceDeleteImage_inNestedIndex(t *testing.T) { - tmpDir := loadFixture(t, "delete_image_nestedindex") - - ref, err := NewReference(tmpDir, "latest") - require.NoError(t, err) - - err = ref.DeleteImage(context.Background(), nil) - require.NoError(t, err) - - // Check that all relevant blobs were deleted/preserved - blobsDir := filepath.Join(tmpDir, "blobs") - blobDoesNotExist(t, blobsDir, "sha256:4a6da698b869046086d0e6ba846f8b931cb33bbaa5c68025b4fd55f67a4f0513") // manifest for the image - blobDoesNotExist(t, blobsDir, "sha256:a527179158cd5cebc11c152b8637b47ce96c838ba2aa0de66d14f45cedc11423") // configuration for the image - blobDoesNotExist(t, blobsDir, "sha256:0c8b263642b51b5c1dc40fe402ae2e97119c6007b6e52146419985ec1f0092dc") // layer used by that image only - blobExists(t, blobsDir, "sha256:d107df792639f1ee2fc4555597cb0eec8978b07e45a68f782965fd00a8964545") // layer used by another image in the index(es) - - // Check that a few new blobs have been created after index deletion/update - blobDoesNotExist(t, blobsDir, "sha256:fbe294d1b627d6ee3c119d558dad8b1c4542cbc51c49ec45dd638921bc5921d0") // nested index 2 that contained the image and only that image - blobDoesNotExist(t, blobsDir, "sha256:b2ff1c27b718b90910711aeda5e02ebbf4440659edd589cc458b3039ea91b35f") // nested index 1, should have been renamed - see next line - blobExists(t, blobsDir, "sha256:13e9f5dde0af5d4303ef0e69d847bc14db6c86a7df616831e126821daf532982") // new sha of the nested index - - // Check that the index has been update with the new nestedindex's sha - ociRef, ok := ref.(ociReference) - require.True(t, ok) - index, err := ociRef.getIndex() - require.NoError(t, err) - assert.Equal(t, 1, len(index.Manifests)) -} - func TestReferenceOCILayoutPath(t *testing.T) { ref, tmpDir := refToTempOCI(t) ociRef, ok := ref.(ociReference)