Skip to content

Commit

Permalink
Add E2E test for pulling images with unorderd tar
Browse files Browse the repository at this point in the history
Signed-off-by: Amit Barve <[email protected]>
  • Loading branch information
ambarve committed Dec 1, 2021
1 parent 7646525 commit b269657
Show file tree
Hide file tree
Showing 3 changed files with 330 additions and 0 deletions.
9 changes: 9 additions & 0 deletions test/cri-containerd/pullimage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const (
testLinkName = "fakelink"
testDirPath = "C:\\Users\\Public"
imageWindowsNanoserverTestImage = "cplatpublic.azurecr.io/timestamp:latest"
imageLinuxUnorderedTar = "cplatpublic.azurecr.io/unordered_tar_image:latest"
)

func Test_PullImageTimestamps(t *testing.T) {
Expand Down Expand Up @@ -102,3 +103,11 @@ func Test_PullImageTimestamps(t *testing.T) {
t.Fatalf("Timestamps not in order. startTimestamp should be less than testdirTimestamp and fakelinkTimestamp")
}
}

func Test_PullImageUnorderedTar(t *testing.T) {
requireFeatures(t, featureLCOW)

// This is a very minimal hand crafted image so it can't run a container. We just
// want to test if pulling this image succeeds.
pullRequiredLCOWImages(t, []string{imageLinuxUnorderedTar})
}
229 changes: 229 additions & 0 deletions test/cri-containerd/test-images/unordered_tar/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
package main

import (
"bytes"
"os/exec"
// "compress/gzip" //
"crypto/sha256"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"

// "time"

digest "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)

func createBlob(blobsDirPath string, blobContents io.Reader) (int64, digest.Digest, error) {
tempFile, err := ioutil.TempFile(blobsDirPath, "")
if err != nil {
return 0, "", errors.Wrapf(err, "failed to create file")
}
defer tempFile.Close()

hasher := sha256.New()
multiWriter := io.MultiWriter(tempFile, hasher)
written, err := io.Copy(multiWriter, blobContents)
if err != nil {
return 0, "", errors.Wrap(err, "failed to copy content")
}
dgst := fmt.Sprintf("sha256:%x", hasher.Sum(nil))
tempFile.Close()

// name the blob file with its digest (excluding the first `sha256:` part)
if err = os.Rename(tempFile.Name(), filepath.Join(blobsDirPath, dgst[7:])); err != nil {
return 0, "", errors.Wrap(err, "renaming content file failed")
}
return written, digest.Digest(dgst), nil
}

func createBlobFromTar(tarPath, blobsDirPath string) (int64, digest.Digest, error) {
srcFile, err := os.Open(tarPath)
if err != nil {
return 0, "", errors.Wrap(err, "failed to open content source file")
}
defer srcFile.Close()

return createBlob(blobsDirPath, srcFile)
}

// createBlobFromStruct converts the given struct into json and creates a blob from that.
func createBlobFromStruct(blobsDirPath string, data interface{}) (int64, digest.Digest, error) {
dataJson, err := json.Marshal(data)
if err != nil {
return 0, "", errors.Wrap(err, "failed to marshal struct")
}

// copy config
buf := bytes.NewBuffer(dataJson)
clen, dgst, err := createBlob(blobsDirPath, buf)
if err != nil {
return 0, "", errors.Wrap(err, "config content write failed")
}
return clen, dgst, nil
}

// createMinimalConfig creates a very minimal but valid image config based on the image
// config definition given here:
// https://github.com/opencontainers/image-spec/blob/main/config.md.
func createMinimalConfig(layers []ocispec.Descriptor) ocispec.Image {
var img ocispec.Image
img.Architecture = "amd64"
img.OS = "linux"
img.RootFS.Type = "layers"
for _, layer := range layers {
img.RootFS.DiffIDs = append(img.RootFS.DiffIDs, layer.Digest)
}
return img
}

func createOciLayoutFile(dirPath string) error {
// create oci layout file
flayout, err := os.Create(filepath.Join(dirPath, "oci-layout"))
if err != nil {
return errors.Wrap(err, "failed to create oci layout")
}
_, err = flayout.Write([]byte(`{"imageLayoutVersion":"1.0.0"}`))
if err != nil {
return errors.Wrap(err, "failed to write layout file")
}
return nil
}

// createImageFromLayerTars creates an OCI compliant image (from given layer tars) that
// can be imported to containerd. Note that the layers might not even be valid container
// image layers and so there is no guarantee that this image will work for running a
// container. Main purpose of this routine is to create an image containing specific tars
// so that we can import this image with containerd and catch any image extraction bugs.
func createImageFromLayerTars(layerTars []string) error {
// A very minimal image must contain following things:
//
// 1. oci layout file:
// A file named `oci-layout` situated at the root of the image tar and containing
// the string `{"imageLayoutVersion":"1.0.0"}`.
//
// 2. Index: The index must be a file named `index.json` and must be situated at
// the root of the image tar. The index should have one descriptor for the image
// manifest. Note that all of the content referenced here onwards will be named
// with their sha256 digest value and will be stored under `./blobs/sha256`
// directory (i.e the image tar should have a blobs/sha256 directory at the root)
//
// 3. A manifest:
// Image manifest is a json file stored under the blobs/sha256 directory. The name
// of this file is its sha256 digest and this digest will be provided in the
// descriptor entry in index.json.

// 4. A config: Image config provides most of the image metadata and the json file
// representing this config should also be stored under blobs/sha256. The manifest
// should provide the digest of the config.

// 5. The image layers: The image layers should be stored under the blobs/sha256
// directory and the layer tar files should be named with their sha256 digest. The
// manifest should provide a descriptor for each of the layers.

// create a directory under which all image files are stored. This directory will be
// converted into the image tar at the end.
tempDirPath, err := ioutil.TempDir("", "imagecreator-imagedir")
if err != nil {
return errors.Wrap(err, "failed to create temporary directory")
}
defer os.RemoveAll(tempDirPath)

sha256dirPath := filepath.Join(tempDirPath, "blobs", "sha256")
err = os.MkdirAll(sha256dirPath, 0777)
if err != nil {
return errors.Wrap(err, "failed to create blobs dir")
}

// copy all layer tar as blobs
layerBlobs := []ocispec.Descriptor{}
for _, layerTar := range layerTars {
llen, dgst, err := createBlobFromTar(layerTar, sha256dirPath)
if err != nil {
return errors.Wrap(err, "layer content write failed")
}
layerBlobs = append(layerBlobs, ocispec.Descriptor{
MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
Digest: dgst,
Size: llen,
})
}

// create image config blob
clen, cdgst, err := createBlobFromStruct(sha256dirPath, createMinimalConfig(layerBlobs))
if err != nil {
return errors.Wrap(err, "failed to create config blob")
}

// create manifest blob
var manifest ocispec.Manifest
manifest.SchemaVersion = 2
manifest.Config.Size = clen
manifest.Config.Digest = cdgst
manifest.Config.MediaType = "application/vnd.docker.container.image.v1+json"
manifest.Layers = layerBlobs
mlen, mdgst, err := createBlobFromStruct(sha256dirPath, manifest)
if err != nil {
return errors.Wrap(err, "failed to crate blob for manifest")
}

// create index file
var index ocispec.Index
index.SchemaVersion = 2
index.Manifests = append(index.Manifests, ocispec.Descriptor{
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
Digest: mdgst,
Size: mlen,
})

indexJson, err := json.Marshal(index)
if err != nil {
return errors.Wrap(err, "failed to marshal index json")
}

findex, err := os.Create(filepath.Join(tempDirPath, "index.json"))
if err != nil {
return errors.Wrap(err, "failed to create index file")
}
_, err = findex.Write(indexJson)
if err != nil {
return errors.Wrap(err, "failed to write index.json")
}

// create oci layout file
if err = createOciLayoutFile(tempDirPath); err != nil {
return err
}

// create tar from the image
tarCmd := exec.Command("tar", "-C", tempDirPath, "-cf", "testimage.tar", ".")
if err = tarCmd.Run(); err != nil {
return errors.Wrap(err, "image tar creation failed")
}
return nil
}

func main() {
tempDir, err := ioutil.TempDir("", "imagecreator-layerdir")
if err != nil {
fmt.Printf("failed to create temp dir: %s\n", err)
return
}
defer os.RemoveAll(tempDir)

layerTars, err := createUnorderedTars(tempDir)
if err != nil {
fmt.Printf("failed to create unordered tars: %s\n", err)
return
}

if err = createImageFromLayerTars(layerTars); err != nil {
fmt.Printf("failed to create image: %s\n", err)
return
}
}
92 changes: 92 additions & 0 deletions test/cri-containerd/test-images/unordered_tar/tar_generator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package main

import (
"archive/tar"
"fmt"
"os"
"path/filepath"

"github.com/pkg/errors"
)

type tarContents struct {
path string
body []byte
}

func writeContentsToTar(tw *tar.Writer, contents []tarContents) error {
for _, file := range contents {
var hdr *tar.Header
isDir := (len(file.body) <= 0)
if isDir {
hdr = &tar.Header{
Name: file.path,
Mode: 0777,
Size: 0,
}
} else {
hdr = &tar.Header{
Name: file.path,
Mode: 0777,
Size: int64(len(file.body)),
}
}
if err := tw.WriteHeader(hdr); err != nil {
return errors.Wrapf(err, "failed to write tar header for file: %s", file.path)
}
if !isDir {
if _, err := tw.Write(file.body); err != nil {
return errors.Wrapf(err, "failed to write contents of file: %s", file.path)
}
}
}
return nil
}

// createUnorderedTars creates 2 tars (each tar representing a layer of a container image)
// containing unordered file entries inside the `dirPath` directory. Returns the array of
// created tar file paths. Note: for now we create 2 tar so that we can include unordered
// whiteout entry. If required this routine can be modified to generate more or less
// tars.
func createUnorderedTars(dirPath string) ([]string, error) {
layers := [][]tarContents{
{
// layer with a few unordered entries
{"data/", []byte{}},
{"root.txt", []byte("inside root.txt")},
{"foo/", []byte{}},
{"A/B/b.txt", []byte("inside b.txt")},
{"A/a.txt", []byte("inside a.txt")},
{"A/", []byte{}},
{"A/B/", []byte{}},
},
{
// layer with unordered whiteout directory
{"A/.wh..wh..opq", []byte{}},
{"foo/xyz.txt", []byte{}},
{"A/c.txt", []byte("inside a.txt")},
{"A/", []byte{}},
{"A/C/", []byte{}},
},
}

generatedTars := []string{}
for i, layer := range layers {
layerPath := filepath.Join(dirPath, fmt.Sprintf("tar%d.tar", i+1))
layerTar, err := os.Create(layerPath)
if err != nil {
return []string{}, errors.Wrapf(err, "failed to create tar at path: %s", layerPath)
}
defer layerTar.Close()

tw := tar.NewWriter(layerTar)
defer tw.Close()
if err = writeContentsToTar(tw, layer); err != nil {
return []string{}, errors.Wrapf(err, "failed to write tar contents for tar : %s", layerPath)
}

generatedTars = append(generatedTars, layerPath)
}

return generatedTars, nil
}

0 comments on commit b269657

Please sign in to comment.