Skip to content

Commit

Permalink
Add LayerWriter for block CIMs
Browse files Browse the repository at this point in the history
This commit adds a layer writer that can be used for extracting an image layer tar into a
Block CIM format.

Existing forked CIM layer writer was renamed to a common base type `cimLayerWriter`.
Forked CIM layer writer & Block CIM layer writer both now extend this common base type to
write layers in that specific format.

Signed-off-by: Amit Barve <[email protected]>
  • Loading branch information
ambarve committed Sep 10, 2024
1 parent efd490e commit f3bf59e
Show file tree
Hide file tree
Showing 7 changed files with 342 additions and 95 deletions.
126 changes: 126 additions & 0 deletions internal/wclayer/cim/block_cim_writer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
//go:build windows

package cim

import (
"context"
"fmt"
"path/filepath"

"github.com/Microsoft/go-winio"
"github.com/Microsoft/hcsshim/pkg/cimfs"
)

// A BlockCIMLayerWriter implements the CIMLayerWriter interface to allow writing
// container image layers in the blocked cim format.
type BlockCIMLayerWriter struct {
*cimLayerWriter
// the layer that we are writing
layer *cimfs.BlockCIM
// parent layers
parentLayers []*cimfs.BlockCIM
// added files maintains a map of all files that have been added to this layer
addedFiles map[string]struct{}
}

var _ CIMLayerWriter = &BlockCIMLayerWriter{}

// NewBlockCIMLayerWriter writes the layer files in the block CIM format.
func NewBlockCIMLayerWriter(ctx context.Context, layer *cimfs.BlockCIM, parentLayers []*cimfs.BlockCIM) (_ *BlockCIMLayerWriter, err error) {
if !cimfs.IsBlockedCimSupported() {
return nil, fmt.Errorf("BlockCIM not supported on this build")
} else if layer.Type != cimfs.BlockCIMTypeSingleFile {
// we only support writing single file CIMs for now because in layer
// writing process we still need to write some files (registry hives)
// outside the CIM. We currently use the parent directory of the CIM (i.e
// the parent directory of block path in this case) for this. This can't
// be reliably done with the block device CIM since the block path
// provided will be a volume path. However, once we get rid of hive rollup
// step during layer import we should be able to support block device
// CIMs.
return nil, ErrBlockCIMWriterNotSupported
}

parentLayerPaths := make([]string, 0, len(parentLayers))
for _, pl := range parentLayers {
if pl.Type != layer.Type {
return nil, ErrBlockCIMParentTypeMismatch
}
parentLayerPaths = append(parentLayerPaths, filepath.Dir(pl.BlockPath))
}

cim, err := cimfs.CreateBlockCIM(layer.BlockPath, "", layer.CimName, layer.Type)
if err != nil {
return nil, fmt.Errorf("error in creating a new cim: %w", err)
}

// std file writer writes registry hives outside the CIM for 2 reasons. 1. We can
// merge the hives of this layer with the parent layer hives and then write the
// merged hives into the CIM. 2. When importing child layer of this layer, we
// have access to the merges hives of this layer.
sfw, err := newStdFileWriter(filepath.Dir(layer.BlockPath), parentLayerPaths)
if err != nil {
return nil, fmt.Errorf("error in creating new standard file writer: %w", err)
}

return &BlockCIMLayerWriter{
layer: layer,
parentLayers: parentLayers,
addedFiles: make(map[string]struct{}),
cimLayerWriter: &cimLayerWriter{
ctx: ctx,
cimWriter: cim,
stdFileWriter: sfw,
layerPath: filepath.Dir(layer.BlockPath),
parentLayerPaths: parentLayerPaths,
},
}, nil
}

// Add adds a file to the layer with given metadata.
func (cw *BlockCIMLayerWriter) Add(name string, fileInfo *winio.FileBasicInfo, fileSize int64, securityDescriptor []byte, extendedAttributes []byte, reparseData []byte) error {
cw.addedFiles[name] = struct{}{}
return cw.cimLayerWriter.Add(name, fileInfo, fileSize, securityDescriptor, extendedAttributes, reparseData)
}

// Remove removes a file that was present in a parent layer from the layer.
func (cw *BlockCIMLayerWriter) Remove(name string) error {
// set active write to nil so that we panic if layer tar is incorrectly formatted.
cw.activeWriter = nil
err := cw.cimWriter.AddTombstone(name)
if err != nil {
return fmt.Errorf("failed to remove file : %w", err)
}
return nil
}

// AddLink adds a hard link to the layer. Note that the link added here is evaluated only
// at the CIM merge time. So an invalid link will not throw an error here.
func (cw *BlockCIMLayerWriter) AddLink(name string, target string) error {
// set active write to nil so that we panic if layer tar is incorrectly formatted.
cw.activeWriter = nil

// when adding links to a block CIM, we need to know if the target file is present
// in this same block CIM or if it is coming from one of the parent layers. If the
// file is in the same CIM we add a standard hard link. If the file is not in the
// same CIM we add a special type of link called merged link. This merged link is
// resolved when all the individual block CIM layers are merged. In order to
// reliably know if the target is a part of the CIM or not, we wait until all
// files are added and then lookup the added entries in a map to make the
// decision.
pendingLinkOp := func(c *cimfs.CimFsWriter) error {
if _, ok := cw.addedFiles[target]; ok {
// target was added in this layer - add a normal link. Once a
// hardlink is added that hardlink also becomes a valid target for
// other links so include it in the map.
cw.addedFiles[name] = struct{}{}
return c.AddLink(target, name)
} else {
// target is from a parent layer - add a merged link
return c.AddMergedLink(target, name)
}
}
cw.pendingOps = append(cw.pendingOps, pendingCimOpFunc(pendingLinkOp))
return nil

}
47 changes: 47 additions & 0 deletions internal/wclayer/cim/cim_writer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//go:build windows

package cim

import (
"context"
"errors"
"testing"

"github.com/Microsoft/hcsshim/pkg/cimfs"
)

func TestSingleFileWriterTypeMismatch(t *testing.T) {
layer := &cimfs.BlockCIM{
Type: cimfs.BlockCIMTypeSingleFile,
BlockPath: "",
CimName: "",
}

parent := &cimfs.BlockCIM{
Type: cimfs.BlockCIMTypeDevice,
BlockPath: "",
CimName: "",
}

_, err := NewBlockCIMLayerWriter(context.TODO(), layer, []*cimfs.BlockCIM{parent})
if errors.Is(err, ErrBlockCIMParentTypeMismatch) {
t.Fatalf("expected error `%s`, got `%s`", ErrBlockCIMParentTypeMismatch, err)
}
}

func TestSingleFileWriterInvalidBlockType(t *testing.T) {
layer := &cimfs.BlockCIM{
BlockPath: "",
CimName: "",
}

parent := &cimfs.BlockCIM{
BlockPath: "",
CimName: "",
}

_, err := NewBlockCIMLayerWriter(context.TODO(), layer, []*cimfs.BlockCIM{parent})
if errors.Is(err, ErrBlockCIMWriterNotSupported) {
t.Fatalf("expected error `%s`, got `%s`", ErrBlockCIMWriterNotSupported, err)
}
}
142 changes: 53 additions & 89 deletions internal/wclayer/cim/LayerWriter.go → internal/wclayer/cim/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,39 +10,14 @@ import (
"strings"

"github.com/Microsoft/go-winio"
"github.com/Microsoft/hcsshim/internal/oc"
"github.com/Microsoft/hcsshim/internal/wclayer"
"github.com/Microsoft/hcsshim/pkg/cimfs"
"go.opencensus.io/trace"
)

// A CimLayerWriter implements the wclayer.LayerWriter interface to allow writing container
// image layers in the cim format.
// A cim layer consist of cim files (which are usually stored in the `cim-layers` directory and
// some other files which are stored in the directory of that layer (i.e the `path` directory).
type CimLayerWriter struct {
ctx context.Context
s *trace.Span
// path to the layer (i.e layer's directory) as provided by the caller.
// Even if a layer is stored as a cim in the cim directory, some files associated
// with a layer are still stored in this path.
layerPath string
// parent layer paths
parentLayerPaths []string
// Handle to the layer cim - writes to the cim file
cimWriter *cimfs.CimFsWriter
// Handle to the writer for writing files in the local filesystem
stdFileWriter *stdFileWriter
// reference to currently active writer either cimWriter or stdFileWriter
activeWriter io.Writer
// denotes if this layer has the UtilityVM directory
hasUtilityVM bool
// some files are written outside the cim during initial import (via stdFileWriter) because we need to
// make some modifications to these files before writing them to the cim. The pendingOps slice
// maintains a list of such delayed modifications to the layer cim. These modifications are applied at
// the very end of layer import process.
pendingOps []pendingCimOp
}
var (
ErrBlockCIMWriterNotSupported = fmt.Errorf("writing block device CIM isn't supported")
ErrBlockCIMParentTypeMismatch = fmt.Errorf("parent layer block CIM type doesn't match with extraction layer")
)

type hive struct {
name string
Expand All @@ -60,6 +35,24 @@ var (
}
)

// CIMLayerWriter is an interface that supports writing a new container image layer to the
// CIM format
type CIMLayerWriter interface {
// Add adds a file to the layer with given metadata.
Add(string, *winio.FileBasicInfo, int64, []byte, []byte, []byte) error
// AddLink adds a hard link to the layer. The target must already have been added.
AddLink(string, string) error
// AddAlternateStream adds an alternate stream to a file
AddAlternateStream(string, uint64) error
// Remove removes a file that was present in a parent layer from the layer.
Remove(string) error
// Write writes data to the current file. The data must be in the format of a Win32
// backup stream.
Write([]byte) (int, error)
// Close finishes the layer writing process and releases any resources.
Close(context.Context) error
}

func isDeltaOrBaseHive(path string) bool {
for _, hv := range hives {
if strings.EqualFold(path, filepath.Join(wclayer.HivesPath, hv.delta)) ||
Expand All @@ -79,8 +72,33 @@ func isStdFile(path string) bool {
path == wclayer.BcdFilePath || path == wclayer.BootMgrFilePath)
}

// cimLayerWriter is a base struct that is further extended by forked cim writer & blocked
// cim writer to provide full functionality of writing layers.
type cimLayerWriter struct {
ctx context.Context
// Handle to the layer cim - writes to the cim file
cimWriter *cimfs.CimFsWriter
// Handle to the writer for writing files in the local filesystem
stdFileWriter *stdFileWriter
// reference to currently active writer either cimWriter or stdFileWriter
activeWriter io.Writer
// denotes if this layer has the UtilityVM directory
hasUtilityVM bool
// path to the layer (i.e layer's directory) as provided by the caller.
// Even if a layer is stored as a cim in the cim directory, some files associated
// with a layer are still stored in this path.
layerPath string
// parent layer paths
parentLayerPaths []string
// some files are written outside the cim during initial import (via stdFileWriter) because we need to
// make some modifications to these files before writing them to the cim. The pendingOps slice
// maintains a list of such delayed modifications to the layer cim. These modifications are applied at
// the very end of layer import process.
pendingOps []pendingCimOp
}

// Add adds a file to the layer with given metadata.
func (cw *CimLayerWriter) Add(name string, fileInfo *winio.FileBasicInfo, fileSize int64, securityDescriptor []byte, extendedAttributes []byte, reparseData []byte) error {
func (cw *cimLayerWriter) Add(name string, fileInfo *winio.FileBasicInfo, fileSize int64, securityDescriptor []byte, extendedAttributes []byte, reparseData []byte) error {
if name == wclayer.UtilityVMPath {
cw.hasUtilityVM = true
}
Expand Down Expand Up @@ -108,7 +126,7 @@ func (cw *CimLayerWriter) Add(name string, fileInfo *winio.FileBasicInfo, fileSi
}

// AddLink adds a hard link to the layer. The target must already have been added.
func (cw *CimLayerWriter) AddLink(name string, target string) error {
func (cw *cimLayerWriter) AddLink(name string, target string) error {
// set active write to nil so that we panic if layer tar is incorrectly formatted.
cw.activeWriter = nil
if isStdFile(target) {
Expand All @@ -130,7 +148,7 @@ func (cw *CimLayerWriter) AddLink(name string, target string) error {

// AddAlternateStream creates another alternate stream at the given
// path. Any writes made after this call will go to that stream.
func (cw *CimLayerWriter) AddAlternateStream(name string, size uint64) error {
func (cw *cimLayerWriter) AddAlternateStream(name string, size uint64) error {
if isStdFile(name) {
// As of now there is no known case of std file having multiple data streams.
// If such a file is encountered our assumptions are wrong. Error out.
Expand All @@ -144,21 +162,14 @@ func (cw *CimLayerWriter) AddAlternateStream(name string, size uint64) error {
return nil
}

// Remove removes a file that was present in a parent layer from the layer.
func (cw *CimLayerWriter) Remove(name string) error {
// set active write to nil so that we panic if layer tar is incorrectly formatted.
cw.activeWriter = nil
return cw.cimWriter.Unlink(name)
}

// Write writes data to the current file. The data must be in the format of a Win32
// backup stream.
func (cw *CimLayerWriter) Write(b []byte) (int, error) {
func (cw *cimLayerWriter) Write(b []byte) (int, error) {
return cw.activeWriter.Write(b)
}

// Close finishes the layer writing process and releases any resources.
func (cw *CimLayerWriter) Close(ctx context.Context) (retErr error) {
func (cw *cimLayerWriter) Close(ctx context.Context) (retErr error) {
if err := cw.stdFileWriter.Close(ctx); err != nil {
return err
}
Expand All @@ -170,7 +181,7 @@ func (cw *CimLayerWriter) Close(ctx context.Context) (retErr error) {
}
}()

// UVM based containers aren't supported with CimFS, don't process the UVM layer
// Find out the osversion of this layer, both base & non-base layers can have UtilityVM layer.
processUtilityVM := false

if len(cw.parentLayerPaths) == 0 {
Expand All @@ -190,50 +201,3 @@ func (cw *CimLayerWriter) Close(ctx context.Context) (retErr error) {
}
return nil
}

func NewCimLayerWriter(ctx context.Context, layerPath, cimPath string, parentLayerPaths, parentLayerCimPaths []string) (_ *CimLayerWriter, err error) {
if !cimfs.IsCimFSSupported() {
return nil, fmt.Errorf("CimFs not supported on this build")
}

ctx, span := trace.StartSpan(ctx, "hcsshim::NewCimLayerWriter")
defer func() {
if err != nil {
oc.SetSpanStatus(span, err)
span.End()
}
}()
span.AddAttributes(
trace.StringAttribute("path", layerPath),
trace.StringAttribute("cimPath", cimPath),
trace.StringAttribute("parentLayerPaths", strings.Join(parentLayerCimPaths, ", ")),
trace.StringAttribute("parentLayerPaths", strings.Join(parentLayerPaths, ", ")))

parentCim := ""
if len(parentLayerPaths) > 0 {
if filepath.Dir(cimPath) != filepath.Dir(parentLayerCimPaths[0]) {
return nil, fmt.Errorf("parent cim can not be stored in different directory")
}
// We only need to provide parent CIM name, it is assumed that both parent CIM
// and newly created CIM are present in the same directory.
parentCim = filepath.Base(parentLayerCimPaths[0])
}

cim, err := cimfs.Create(filepath.Dir(cimPath), parentCim, filepath.Base(cimPath))
if err != nil {
return nil, fmt.Errorf("error in creating a new cim: %w", err)
}

sfw, err := newStdFileWriter(layerPath, parentLayerPaths)
if err != nil {
return nil, fmt.Errorf("error in creating new standard file writer: %w", err)
}
return &CimLayerWriter{
ctx: ctx,
s: span,
layerPath: layerPath,
parentLayerPaths: parentLayerPaths,
cimWriter: cim,
stdFileWriter: sfw,
}, nil
}
Loading

0 comments on commit f3bf59e

Please sign in to comment.