package buildah import ( "archive/tar" "bytes" "context" "encoding/json" "os" "path/filepath" "testing" "time" cp "github.com/containers/image/v5/copy" "github.com/containers/image/v5/signature" imageStorage "github.com/containers/image/v5/storage" "github.com/containers/image/v5/transports/alltransports" "github.com/containers/storage" storageTypes "github.com/containers/storage/types" digest "github.com/opencontainers/go-digest" ispec "github.com/opencontainers/image-spec/specs-go" v1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) type testRetryCopyImageWrappedStore struct { phantomImageID string storage.Store } func (ts *testRetryCopyImageWrappedStore) CreateImage(id string, names []string, layer, metadata string, options *storage.ImageOptions) (*storage.Image, error) { if id == ts.phantomImageID { if img, err := ts.Store.Image(id); img != nil && err == nil { // i'm another thread somewhere if _, err := ts.Store.DeleteImage(id, true); err != nil { return nil, err } } } return ts.Store.CreateImage(id, names, layer, metadata, options) } func TestRetryCopyImage(t *testing.T) { t.Parallel() ctx := context.TODO() graphDriverName := os.Getenv("STORAGE_DRIVER") if graphDriverName == "" { graphDriverName = "vfs" } store, err := storage.GetStore(storageTypes.StoreOptions{ RunRoot: t.TempDir(), GraphRoot: t.TempDir(), GraphDriverName: graphDriverName, }) require.NoError(t, err, "initializing storage") t.Cleanup(func() { _, err := store.Shutdown(true); assert.NoError(t, err) }) // construct an "image" that can be pulled into local storage var layerBuffer bytes.Buffer tw := tar.NewWriter(&layerBuffer) err = tw.WriteHeader(&tar.Header{ Name: "rootfile", Typeflag: tar.TypeReg, Size: 1234, }) require.NoError(t, err, "writing header for archive") _, err = tw.Write(make([]byte, 1234)) require.NoError(t, err, "writing empty file to archive") require.NoError(t, tw.Close(), "finishing layer") layerDigest := digest.Canonical.FromBytes(layerBuffer.Bytes()) imageConfig := v1.Image{ RootFS: v1.RootFS{ Type: "layers", DiffIDs: []digest.Digest{layerDigest}, }, } imageConfigBytes, err := json.Marshal(&imageConfig) require.NoError(t, err, "marshalling image configuration blob") imageConfigDigest := digest.Canonical.FromBytes(imageConfigBytes) imageManifest := v1.Manifest{ Versioned: ispec.Versioned{ SchemaVersion: 2, }, MediaType: v1.MediaTypeImageManifest, Config: v1.Descriptor{ MediaType: v1.MediaTypeImageConfig, Size: int64(len(imageConfigBytes)), Digest: digest.FromBytes(imageConfigBytes), }, Layers: []v1.Descriptor{ { MediaType: v1.MediaTypeImageLayer, Size: int64(layerBuffer.Len()), Digest: layerDigest, }, }, } imageManifestBytes, err := json.Marshal(&imageManifest) require.NoError(t, err, "marshalling image manifest") imageManifestDigest := digest.Canonical.FromBytes(imageManifestBytes) // write it to an oci layout ociDir := t.TempDir() blobbyDir := filepath.Join(ociDir, "blobs") require.NoError(t, os.Mkdir(blobbyDir, 0o700)) blobDir := filepath.Join(blobbyDir, layerDigest.Algorithm().String()) require.NoError(t, os.Mkdir(blobDir, 0o700)) require.NoError(t, os.WriteFile(filepath.Join(blobDir, layerDigest.Encoded()), layerBuffer.Bytes(), 0o600), "writing layer") require.NoError(t, os.WriteFile(filepath.Join(blobDir, imageConfigDigest.Encoded()), imageConfigBytes, 0o600), "writing image config") require.NoError(t, os.WriteFile(filepath.Join(blobDir, imageManifestDigest.Encoded()), imageManifestBytes, 0o600), "writing manifest") imageIndex := v1.Index{ Versioned: ispec.Versioned{ SchemaVersion: 2, }, MediaType: v1.MediaTypeImageIndex, Manifests: []v1.Descriptor{ { MediaType: v1.MediaTypeImageManifest, Digest: imageManifestDigest, Size: int64(len(imageManifestBytes)), }, }, } imageIndexBytes, err := json.Marshal(&imageIndex) require.NoError(t, err, "marshalling image index") require.NoError(t, os.WriteFile(filepath.Join(ociDir, v1.ImageIndexFile), imageIndexBytes, 0o600), "writing image index") imageLayout := v1.ImageLayout{ Version: v1.ImageLayoutVersion, } imageLayoutBytes, err := json.Marshal(&imageLayout) require.NoError(t, err, "marshalling image layout") require.NoError(t, os.WriteFile(filepath.Join(ociDir, v1.ImageLayoutFile), imageLayoutBytes, 0o600), "writing image layout") // pull the image, twice, just to make sure nothing weird happens srcRef, err := alltransports.ParseImageName("oci:" + ociDir) require.NoError(t, err, "building reference to image layout") destRef, err := imageStorage.Transport.NewStoreReference(store, nil, imageConfigDigest.Encoded()) require.NoError(t, err, "building reference to image in store") policy, err := signature.NewPolicyFromFile("tests/policy.json") require.NoError(t, err, "reading signature policy") policyContext, err := signature.NewPolicyContext(policy) require.NoError(t, err, "building policy context") t.Cleanup(func() { require.NoError(t, policyContext.Destroy(), "destroying policy context") }) _, err = retryCopyImage(ctx, policyContext, destRef, srcRef, destRef, &cp.Options{}, 3, 1*time.Second) require.NoError(t, err, "copying image") _, err = retryCopyImage(ctx, policyContext, destRef, srcRef, destRef, &cp.Options{}, 3, 1*time.Second) require.NoError(t, err, "copying image") // now make something weird happen wrappedStore := &testRetryCopyImageWrappedStore{ phantomImageID: imageConfigDigest.Encoded(), Store: store, } wrappedDestRef, err := imageStorage.Transport.NewStoreReference(wrappedStore, nil, imageConfigDigest.Encoded()) require.NoError(t, err, "building wrapped reference") // copy with retry-on-storage-layer-unknown = false: expect an error // (if it succeeds, either the test is broken, or we can remove this // case from the retry function) _, err = retryCopyImageWithOptions(ctx, policyContext, wrappedDestRef, srcRef, wrappedDestRef, &cp.Options{}, 3, 1*time.Second, false) require.ErrorIs(t, err, storage.ErrLayerUnknown, "copying image") // copy with retry-on-storage-layer-unknown = true: expect no error _, err = retryCopyImageWithOptions(ctx, policyContext, wrappedDestRef, srcRef, wrappedDestRef, &cp.Options{}, 3, 1*time.Second, true) require.NoError(t, err, "copying image") }