Skip to content

Commit

Permalink
Add support for creating and mounting merged CIMs.
Browse files Browse the repository at this point in the history
CimFS supports merging CIMs and then mounting those merged CIMs as an alternative to creating forked
CIMs. Advantage of merging CIMs that each CIM can be stored in its own directory. Unlike forked ICMs there is
no requirement that all CIMs need to be present in the same directory. This commit adds the Go wrappers and
some tests for using the new CIM merging APIs.

Also, fixes a bug related to the return value of CimCloseImage API. This API has void return so we shouldn't
be returning anything in our wrapper.

Signed-off-by: Amit Barve <[email protected]>
  • Loading branch information
ambarve committed May 27, 2024
1 parent 43d1ab5 commit de3d695
Show file tree
Hide file tree
Showing 6 changed files with 241 additions and 51 deletions.
9 changes: 8 additions & 1 deletion internal/winapi/cimfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,16 @@ type CimFsFileMetadata struct {
EACount uint32
}

type CimFsImagePath struct {
ImageDir *uint16
ImageName *uint16
}

//sys CimMountImage(imagePath string, fsName string, flags uint32, volumeID *g) (hr error) = cimfs.CimMountImage?
//sys CimDismountImage(volumeID *g) (hr error) = cimfs.CimDismountImage?

//sys CimCreateImage(imagePath string, oldFSName *uint16, newFSName *uint16, cimFSHandle *FsHandle) (hr error) = cimfs.CimCreateImage?
//sys CimCloseImage(cimFSHandle FsHandle) = cimfs.CimCloseImage?
//sys CimCloseImage(cimFSHandle FsHandle) = cimfs.CimCloseImage
//sys CimCommitImage(cimFSHandle FsHandle) (hr error) = cimfs.CimCommitImage?

//sys CimCreateFile(cimFSHandle FsHandle, path string, file *CimFsFileMetadata, cimStreamHandle *StreamHandle) (hr error) = cimfs.CimCreateFile?
Expand All @@ -45,3 +50,5 @@ type CimFsFileMetadata struct {
//sys CimDeletePath(cimFSHandle FsHandle, path string) (hr error) = cimfs.CimDeletePath?
//sys CimCreateHardLink(cimFSHandle FsHandle, newPath string, oldPath string) (hr error) = cimfs.CimCreateHardLink?
//sys CimCreateAlternateStream(cimFSHandle FsHandle, path string, size uint64, cimStreamHandle *StreamHandle) (hr error) = cimfs.CimCreateAlternateStream?
//sys CimAddFsToMergedImage(cimFSHandle FsHandle, path string) (hr error) = cimfs.CimAddFsToMergedImage?
//sys CimMergeMountImage(numCimPaths uint32, backingImagePaths *CimFsImagePath, flags uint32, volumeID *g) (hr error) = cimfs.CimMergeMountImage?
43 changes: 40 additions & 3 deletions internal/winapi/zsyscall_windows.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

163 changes: 119 additions & 44 deletions pkg/cimfs/cim_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,41 +60,34 @@ func createCimFileUtil(c *CimFsWriter, fileTuple tuple) error {
return nil
}

// This test creates a cim, writes some files to it and then reads those files back.
// The cim created by this test has only 3 files in the following tree
// /
// |- foobar.txt
// |- foo
// |--- bar.txt
func TestCimReadWrite(t *testing.T) {
if !IsCimFSSupported() {
t.Skipf("CimFs not supported")
}
// openNewCIM creates a new CIM inside `dirPath` with name `name` and returns a writer to that CIM.
// The caller MUST commit the CIM & close the writer.
func openNewCIM(t *testing.T, name, dirPath string) *CimFsWriter {
t.Helper()

testContents := []tuple{
{"foobar.txt", []byte("foobar test data"), false},
{"foo", []byte(""), true},
{"foo\\bar.txt", []byte("bar test data"), false},
}

tempDir := t.TempDir()

cimName := "test.cim"
cimPath := filepath.Join(tempDir, cimName)
c, err := Create(tempDir, "", cimName)
cimPath := filepath.Join(dirPath, name)
c, err := Create(dirPath, "", name)
if err != nil {
t.Fatalf("failed while creating a cim: %s", err)
}
defer func() {
t.Cleanup(func() {
// destroy cim sometimes fails if tried immediately after accessing & unmounting the cim so
// give some time and then remove.
time.Sleep(3 * time.Second)
if err := DestroyCim(context.Background(), cimPath); err != nil {
t.Fatalf("destroy cim failed: %s", err)
}
}()
})
return c
}

for _, ft := range testContents {
// writeNewCIM creates a new CIM with `name` inside directory `dirPath` and writes the
// given data inside it. The CIM is then committed and closed.
func writeNewCIM(t *testing.T, name, dirPath string, contents []tuple) {
t.Helper()

c := openNewCIM(t, name, dirPath)
for _, ft := range contents {
err := createCimFileUtil(c, ft)
if err != nil {
t.Fatalf("failed to create the file %s inside the cim:%s", ft.filepath, err)
Expand All @@ -103,33 +96,23 @@ func TestCimReadWrite(t *testing.T) {
if err := c.Close(); err != nil {
t.Fatalf("cim close: %s", err)
}
}

// mount and read the contents of the cim
volumeGUID, err := guid.NewV4()
if err != nil {
t.Fatalf("generate cim mount GUID: %s", err)
}

mountvol, err := Mount(cimPath, volumeGUID, hcsschema.CimMountFlagCacheFiles)
if err != nil {
t.Fatalf("mount cim : %s", err)
}
defer func() {
if err := Unmount(mountvol); err != nil {
t.Fatalf("unmount failed: %s", err)
}
}()
// compareContent takes in path to a directory (which is usually a volume at which a CIM is mounted) and
// ensures that every file/directory in the `testContents` shows up exactly as it is under that directory.
func compareContent(t *testing.T, root string, testContents []tuple) {
t.Helper()

for _, ft := range testContents {
if ft.isDir {
_, err := os.Stat(filepath.Join(mountvol, ft.filepath))
_, err := os.Stat(filepath.Join(root, ft.filepath))
if err != nil {
t.Fatalf("stat directory %s from cim: %s", ft.filepath, err)
}
} else {
f, err := os.Open(filepath.Join(mountvol, ft.filepath))
f, err := os.Open(filepath.Join(root, ft.filepath))
if err != nil {
t.Fatalf("open file %s: %s", filepath.Join(mountvol, ft.filepath), err)
t.Fatalf("open file %s: %s", filepath.Join(root, ft.filepath), err)
}
defer f.Close()

Expand All @@ -139,11 +122,103 @@ func TestCimReadWrite(t *testing.T) {
rc, err := f.Read(fileContents)
if err != nil && !errors.Is(err, io.EOF) {
t.Fatalf("failure while reading file %s from cim: %s", ft.filepath, err)
} else if rc != len(ft.fileContents) {
t.Fatalf("couldn't read complete file contents for file: %s, read %d bytes, expected: %d", ft.filepath, rc, len(ft.fileContents))
} else if !bytes.Equal(fileContents[:rc], ft.fileContents) {
t.Fatalf("contents of file %s don't match", ft.filepath)
}
}
}
}

// This test creates a cim, writes some files to it and then reads those files back.
// The cim created by this test has only 3 files in the following tree
// /
// |- foobar.txt
// |- foo
// |--- bar.txt
func TestCimReadWrite(t *testing.T) {
if !IsCimFSSupported() {
t.Skipf("CimFs not supported")
}

testContents := []tuple{
{"foobar.txt", []byte("foobar test data"), false},
{"foo", []byte(""), true},
{"foo\\bar.txt", []byte("bar test data"), false},
}

tempDir := t.TempDir()

writeNewCIM(t, "test.cim", tempDir, testContents)

// mount and read the contents of the cim
volumeGUID, err := guid.NewV4()
if err != nil {
t.Fatalf("generate cim mount GUID: %s", err)
}

mountvol, err := Mount(filepath.Join(tempDir, "test.cim"), volumeGUID, hcsschema.CimMountFlagCacheFiles)
if err != nil {
t.Fatalf("mount cim : %s", err)
}
defer func() {
if err := Unmount(mountvol); err != nil {
t.Fatalf("unmount failed: %s", err)
}
}()

compareContent(t, mountvol, testContents)
}

// This test creates two CIMs, writes some files to them, then merges those CIMs, mounts the merged CIM and reads the files back.
func TestMergedCims(t *testing.T) {
if !IsMergedCimSupported() {
t.Skipf("merged CIMs are not supported")
}

cim1Dir := t.TempDir()
cim1Contents := []tuple{
{"f1.txt", []byte("f1"), false},
{"f2.txt", []byte("f2"), false},
}
cim1Name := "test1.cim"
cim1Path := filepath.Join(cim1Dir, cim1Name)
writeNewCIM(t, cim1Name, cim1Dir, cim1Contents)

cim2Dir := t.TempDir()
cim2Contents := []tuple{
{"f1.txt", []byte("f1overwrite"), false}, // overwrite file from lower layer
{"f3.txt", []byte("f3"), false},
}
cim2Name := "test2.cim"
cim2Path := filepath.Join(cim2Dir, cim2Name)
writeNewCIM(t, cim2Name, cim2Dir, cim2Contents)

// create a merged CIM in 2nd CIMs directory
mergedName := "testmerged.cim"
mergedPath := filepath.Join(cim2Dir, mergedName)
// order of CIMs in topmost first and bottom most last
err := CreateMergedCim(cim2Dir, mergedName, []string{cim2Path, cim1Path})
if err != nil {
t.Fatalf("failed to merge CIMs: %s", err)
}

// mount and read the contents of the cim
volumeGUID, err := guid.NewV4()
if err != nil {
t.Fatalf("generate cim mount GUID: %s", err)
}

mountvol, err := MountMergedCims([]string{cim1Path, cim2Path}, mergedPath, hcsschema.CimMountFlagCacheFiles, volumeGUID)
if err != nil {
t.Fatalf("mount cim failed: %s", err)
}
defer func() {
if err := Unmount(mountvol); err != nil {
t.Fatalf("unmount failed: %s", err)
}
}()

// we expect to find f1 (overwritten), f2 & f3
allContent := []tuple{cim1Contents[1], cim2Contents[0], cim2Contents[1]}
compareContent(t, mountvol, allContent)
}
33 changes: 30 additions & 3 deletions pkg/cimfs/cim_writer_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,35 @@ func Create(imagePath string, oldFSName string, newFSName string) (_ *CimFsWrite
return &CimFsWriter{handle: handle, name: filepath.Join(imagePath, fsName)}, nil
}

// CreateMergedCim creates a new merged CIM from the CIMs provided in the `cimPaths` array. CIM at index 0 is
// considered to be the base CIM and the CIM at index `cimPath.length-1` is considered the topmost CIM
// on. When mounting this merged CIM the source CIMs MUST be provided in the same order. Ensure all CIM paths
// are absolute paths. `imageDir` is the directory inside which the merged cim is stored with the name `name`.
func CreateMergedCim(imageDir, name string, cimPaths []string) (err error) {
if !IsMergedCimSupported() {
return ErrMergedCimNotSupported
}
cim, err := Create(imageDir, "", name)
if err != nil {
return fmt.Errorf("create cim image: %w", err)
}
defer winapi.CimCloseImage(cim.handle)

// CimAddFsToMergedImage expects that topmost CIM is added first and the bottom most CIM is added
// last.
for i := len(cimPaths) - 1; i >= 0; i-- {
if err := winapi.CimAddFsToMergedImage(cim.handle, cimPaths[i]); err != nil {
return fmt.Errorf("add cim to merged image: %w", err)
}
}

err = winapi.CimCommitImage(cim.handle)
if err != nil {
return fmt.Errorf("commit merged image: %w", err)
}
return nil
}

// CreateAlternateStream creates alternate stream of given size at the given path inside the cim. This will
// replace the current active stream. Always, finish writing current active stream and then create an
// alternate stream.
Expand Down Expand Up @@ -210,9 +239,7 @@ func (c *CimFsWriter) Close() error {
if err := c.commit(); err != nil {
return &OpError{Cim: c.name, Op: "commit", Err: err}
}
if err := winapi.CimCloseImage(c.handle); err != nil {
return &OpError{Cim: c.name, Op: "close", Err: err}
}
winapi.CimCloseImage(c.handle)
c.handle = 0
return nil
}
Expand Down
10 changes: 10 additions & 0 deletions pkg/cimfs/cimfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,24 @@
package cimfs

import (
"fmt"

"github.com/Microsoft/hcsshim/osversion"
"github.com/sirupsen/logrus"
)

var (
ErrMergedCimNotSupported = fmt.Errorf("merged CIMs are not supported on this OS version")
)

func IsCimFSSupported() bool {
rv, err := osversion.BuildRevision()
if err != nil {
logrus.WithError(err).Warn("get build revision")
}
return osversion.Build() == 20348 && rv >= 2031
}

func IsMergedCimSupported() bool {
return osversion.Build() >= 26100
}
Loading

0 comments on commit de3d695

Please sign in to comment.