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.

This commit also removes some code that used `time.Now()` as the default timestamps for
some files that it creates within the layer CIM. These timestamps cause differences in the
layer CIMs generated from the same layer tar. This change fixes that.

Signed-off-by: Amit Barve <[email protected]>
  • Loading branch information
ambarve committed Oct 14, 2024
1 parent e222cc9 commit f9095a8
Show file tree
Hide file tree
Showing 7 changed files with 342 additions and 108 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.IsBlockCimSupported() {
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 f9095a8

Please sign in to comment.