Skip to content

Commit c294ae7

Browse files
committed
Add LayerWriter for block CIMs
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]>
1 parent 3d48537 commit c294ae7

File tree

8 files changed

+367
-108
lines changed

8 files changed

+367
-108
lines changed
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
//go:build windows
2+
3+
package cim
4+
5+
import (
6+
"context"
7+
"fmt"
8+
"path/filepath"
9+
10+
"github.com/Microsoft/go-winio"
11+
"github.com/Microsoft/hcsshim/internal/log"
12+
"github.com/Microsoft/hcsshim/pkg/cimfs"
13+
)
14+
15+
// A BlockCIMLayerWriter implements the CIMLayerWriter interface to allow writing
16+
// container image layers in the blocked cim format.
17+
type BlockCIMLayerWriter struct {
18+
*cimLayerWriter
19+
// the layer that we are writing
20+
layer *cimfs.BlockCIM
21+
// parent layers
22+
parentLayers []*cimfs.BlockCIM
23+
// added files maintains a map of all files that have been added to this layer
24+
addedFiles map[string]struct{}
25+
}
26+
27+
var _ CIMLayerWriter = &BlockCIMLayerWriter{}
28+
29+
// NewBlockCIMLayerWriter writes the layer files in the block CIM format.
30+
func NewBlockCIMLayerWriter(ctx context.Context, layer *cimfs.BlockCIM, parentLayers []*cimfs.BlockCIM) (_ *BlockCIMLayerWriter, err error) {
31+
if !cimfs.IsBlockCimSupported() {
32+
return nil, fmt.Errorf("BlockCIM not supported on this build")
33+
} else if layer.Type != cimfs.BlockCIMTypeSingleFile {
34+
// we only support writing single file CIMs for now because in layer
35+
// writing process we still need to write some files (registry hives)
36+
// outside the CIM. We currently use the parent directory of the CIM (i.e
37+
// the parent directory of block path in this case) for this. This can't
38+
// be reliably done with the block device CIM since the block path
39+
// provided will be a volume path. However, once we get rid of hive rollup
40+
// step during layer import we should be able to support block device
41+
// CIMs.
42+
return nil, ErrBlockCIMWriterNotSupported
43+
}
44+
45+
parentLayerPaths := make([]string, 0, len(parentLayers))
46+
for _, pl := range parentLayers {
47+
if pl.Type != layer.Type {
48+
return nil, ErrBlockCIMParentTypeMismatch
49+
}
50+
parentLayerPaths = append(parentLayerPaths, filepath.Dir(pl.BlockPath))
51+
}
52+
53+
cim, err := cimfs.CreateBlockCIM(layer.BlockPath, layer.CimName, layer.Type)
54+
if err != nil {
55+
return nil, fmt.Errorf("error in creating a new cim: %w", err)
56+
}
57+
defer func() {
58+
if err != nil {
59+
cErr := cim.Close()
60+
if cErr != nil {
61+
log.G(ctx).WithError(err).Warnf("failed to close cim after error: %s", cErr)
62+
}
63+
}
64+
}()
65+
66+
// std file writer writes registry hives outside the CIM for 2 reasons. 1. We can
67+
// merge the hives of this layer with the parent layer hives and then write the
68+
// merged hives into the CIM. 2. When importing child layer of this layer, we
69+
// have access to the merges hives of this layer.
70+
sfw, err := newStdFileWriter(filepath.Dir(layer.BlockPath), parentLayerPaths)
71+
if err != nil {
72+
return nil, fmt.Errorf("error in creating new standard file writer: %w", err)
73+
}
74+
75+
return &BlockCIMLayerWriter{
76+
layer: layer,
77+
parentLayers: parentLayers,
78+
addedFiles: make(map[string]struct{}),
79+
cimLayerWriter: &cimLayerWriter{
80+
ctx: ctx,
81+
cimWriter: cim,
82+
stdFileWriter: sfw,
83+
layerPath: filepath.Dir(layer.BlockPath),
84+
parentLayerPaths: parentLayerPaths,
85+
},
86+
}, nil
87+
}
88+
89+
// Add adds a file to the layer with given metadata.
90+
func (cw *BlockCIMLayerWriter) Add(name string, fileInfo *winio.FileBasicInfo, fileSize int64, securityDescriptor []byte, extendedAttributes []byte, reparseData []byte) error {
91+
cw.addedFiles[name] = struct{}{}
92+
return cw.cimLayerWriter.Add(name, fileInfo, fileSize, securityDescriptor, extendedAttributes, reparseData)
93+
}
94+
95+
// Remove removes a file that was present in a parent layer from the layer.
96+
func (cw *BlockCIMLayerWriter) Remove(name string) error {
97+
// set active write to nil so that we panic if layer tar is incorrectly formatted.
98+
cw.activeWriter = nil
99+
err := cw.cimWriter.AddTombstone(name)
100+
if err != nil {
101+
return fmt.Errorf("failed to remove file : %w", err)
102+
}
103+
return nil
104+
}
105+
106+
// AddLink adds a hard link to the layer. Note that the link added here is evaluated only
107+
// at the CIM merge time. So an invalid link will not throw an error here.
108+
func (cw *BlockCIMLayerWriter) AddLink(name string, target string) error {
109+
// set active write to nil so that we panic if layer tar is incorrectly formatted.
110+
cw.activeWriter = nil
111+
112+
// when adding links to a block CIM, we need to know if the target file is present
113+
// in this same block CIM or if it is coming from one of the parent layers. If the
114+
// file is in the same CIM we add a standard hard link. If the file is not in the
115+
// same CIM we add a special type of link called merged link. This merged link is
116+
// resolved when all the individual block CIM layers are merged. In order to
117+
// reliably know if the target is a part of the CIM or not, we wait until all
118+
// files are added and then lookup the added entries in a map to make the
119+
// decision.
120+
pendingLinkOp := func(c *cimfs.CimFsWriter) error {
121+
if _, ok := cw.addedFiles[target]; ok {
122+
// target was added in this layer - add a normal link. Once a
123+
// hardlink is added that hardlink also becomes a valid target for
124+
// other links so include it in the map.
125+
cw.addedFiles[name] = struct{}{}
126+
return c.AddLink(target, name)
127+
} else {
128+
// target is from a parent layer - add a merged link
129+
return c.AddMergedLink(target, name)
130+
}
131+
}
132+
cw.pendingOps = append(cw.pendingOps, pendingCimOpFunc(pendingLinkOp))
133+
return nil
134+
135+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//go:build windows
2+
3+
package cim
4+
5+
import (
6+
"context"
7+
"errors"
8+
"testing"
9+
10+
"github.com/Microsoft/hcsshim/pkg/cimfs"
11+
)
12+
13+
func TestSingleFileWriterTypeMismatch(t *testing.T) {
14+
layer := &cimfs.BlockCIM{
15+
Type: cimfs.BlockCIMTypeSingleFile,
16+
BlockPath: "",
17+
CimName: "",
18+
}
19+
20+
parent := &cimfs.BlockCIM{
21+
Type: cimfs.BlockCIMTypeDevice,
22+
BlockPath: "",
23+
CimName: "",
24+
}
25+
26+
_, err := NewBlockCIMLayerWriter(context.TODO(), layer, []*cimfs.BlockCIM{parent})
27+
if !errors.Is(err, ErrBlockCIMParentTypeMismatch) {
28+
t.Fatalf("expected error `%s`, got `%s`", ErrBlockCIMParentTypeMismatch, err)
29+
}
30+
}
31+
32+
func TestSingleFileWriterInvalidBlockType(t *testing.T) {
33+
layer := &cimfs.BlockCIM{
34+
BlockPath: "",
35+
CimName: "",
36+
}
37+
38+
parent := &cimfs.BlockCIM{
39+
BlockPath: "",
40+
CimName: "",
41+
}
42+
43+
_, err := NewBlockCIMLayerWriter(context.TODO(), layer, []*cimfs.BlockCIM{parent})
44+
if !errors.Is(err, ErrBlockCIMWriterNotSupported) {
45+
t.Fatalf("expected error `%s`, got `%s`", ErrBlockCIMWriterNotSupported, err)
46+
}
47+
}
Lines changed: 53 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -10,39 +10,14 @@ import (
1010
"strings"
1111

1212
"github.com/Microsoft/go-winio"
13-
"github.com/Microsoft/hcsshim/internal/oc"
1413
"github.com/Microsoft/hcsshim/internal/wclayer"
1514
"github.com/Microsoft/hcsshim/pkg/cimfs"
16-
"go.opencensus.io/trace"
1715
)
1816

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

4722
type hive struct {
4823
name string
@@ -60,6 +35,24 @@ var (
6035
}
6136
)
6237

38+
// CIMLayerWriter is an interface that supports writing a new container image layer to the
39+
// CIM format
40+
type CIMLayerWriter interface {
41+
// Add adds a file to the layer with given metadata.
42+
Add(string, *winio.FileBasicInfo, int64, []byte, []byte, []byte) error
43+
// AddLink adds a hard link to the layer. The target must already have been added.
44+
AddLink(string, string) error
45+
// AddAlternateStream adds an alternate stream to a file
46+
AddAlternateStream(string, uint64) error
47+
// Remove removes a file that was present in a parent layer from the layer.
48+
Remove(string) error
49+
// Write writes data to the current file. The data must be in the format of a Win32
50+
// backup stream.
51+
Write([]byte) (int, error)
52+
// Close finishes the layer writing process and releases any resources.
53+
Close(context.Context) error
54+
}
55+
6356
func isDeltaOrBaseHive(path string) bool {
6457
for _, hv := range hives {
6558
if strings.EqualFold(path, filepath.Join(wclayer.HivesPath, hv.delta)) ||
@@ -79,8 +72,33 @@ func isStdFile(path string) bool {
7972
path == wclayer.BcdFilePath || path == wclayer.BootMgrFilePath)
8073
}
8174

75+
// cimLayerWriter is a base struct that is further extended by forked cim writer & blocked
76+
// cim writer to provide full functionality of writing layers.
77+
type cimLayerWriter struct {
78+
ctx context.Context
79+
// Handle to the layer cim - writes to the cim file
80+
cimWriter *cimfs.CimFsWriter
81+
// Handle to the writer for writing files in the local filesystem
82+
stdFileWriter *stdFileWriter
83+
// reference to currently active writer either cimWriter or stdFileWriter
84+
activeWriter io.Writer
85+
// denotes if this layer has the UtilityVM directory
86+
hasUtilityVM bool
87+
// path to the layer (i.e layer's directory) as provided by the caller.
88+
// Even if a layer is stored as a cim in the cim directory, some files associated
89+
// with a layer are still stored in this path.
90+
layerPath string
91+
// parent layer paths
92+
parentLayerPaths []string
93+
// some files are written outside the cim during initial import (via stdFileWriter) because we need to
94+
// make some modifications to these files before writing them to the cim. The pendingOps slice
95+
// maintains a list of such delayed modifications to the layer cim. These modifications are applied at
96+
// the very end of layer import process.
97+
pendingOps []pendingCimOp
98+
}
99+
82100
// Add adds a file to the layer with given metadata.
83-
func (cw *CimLayerWriter) Add(name string, fileInfo *winio.FileBasicInfo, fileSize int64, securityDescriptor []byte, extendedAttributes []byte, reparseData []byte) error {
101+
func (cw *cimLayerWriter) Add(name string, fileInfo *winio.FileBasicInfo, fileSize int64, securityDescriptor []byte, extendedAttributes []byte, reparseData []byte) error {
84102
if name == wclayer.UtilityVMPath {
85103
cw.hasUtilityVM = true
86104
}
@@ -108,7 +126,7 @@ func (cw *CimLayerWriter) Add(name string, fileInfo *winio.FileBasicInfo, fileSi
108126
}
109127

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

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

147-
// Remove removes a file that was present in a parent layer from the layer.
148-
func (cw *CimLayerWriter) Remove(name string) error {
149-
// set active write to nil so that we panic if layer tar is incorrectly formatted.
150-
cw.activeWriter = nil
151-
return cw.cimWriter.Unlink(name)
152-
}
153-
154165
// Write writes data to the current file. The data must be in the format of a Win32
155166
// backup stream.
156-
func (cw *CimLayerWriter) Write(b []byte) (int, error) {
167+
func (cw *cimLayerWriter) Write(b []byte) (int, error) {
157168
return cw.activeWriter.Write(b)
158169
}
159170

160171
// Close finishes the layer writing process and releases any resources.
161-
func (cw *CimLayerWriter) Close(ctx context.Context) (retErr error) {
172+
func (cw *cimLayerWriter) Close(ctx context.Context) (retErr error) {
162173
if err := cw.stdFileWriter.Close(ctx); err != nil {
163174
return err
164175
}
@@ -170,7 +181,7 @@ func (cw *CimLayerWriter) Close(ctx context.Context) (retErr error) {
170181
}
171182
}()
172183

173-
// UVM based containers aren't supported with CimFS, don't process the UVM layer
184+
// We don't support running UtilityVM with CIM layers yet.
174185
processUtilityVM := false
175186

176187
if len(cw.parentLayerPaths) == 0 {
@@ -190,50 +201,3 @@ func (cw *CimLayerWriter) Close(ctx context.Context) (retErr error) {
190201
}
191202
return nil
192203
}
193-
194-
func NewCimLayerWriter(ctx context.Context, layerPath, cimPath string, parentLayerPaths, parentLayerCimPaths []string) (_ *CimLayerWriter, err error) {
195-
if !cimfs.IsCimFSSupported() {
196-
return nil, fmt.Errorf("CimFs not supported on this build")
197-
}
198-
199-
ctx, span := trace.StartSpan(ctx, "hcsshim::NewCimLayerWriter")
200-
defer func() {
201-
if err != nil {
202-
oc.SetSpanStatus(span, err)
203-
span.End()
204-
}
205-
}()
206-
span.AddAttributes(
207-
trace.StringAttribute("path", layerPath),
208-
trace.StringAttribute("cimPath", cimPath),
209-
trace.StringAttribute("parentLayerPaths", strings.Join(parentLayerCimPaths, ", ")),
210-
trace.StringAttribute("parentLayerPaths", strings.Join(parentLayerPaths, ", ")))
211-
212-
parentCim := ""
213-
if len(parentLayerPaths) > 0 {
214-
if filepath.Dir(cimPath) != filepath.Dir(parentLayerCimPaths[0]) {
215-
return nil, fmt.Errorf("parent cim can not be stored in different directory")
216-
}
217-
// We only need to provide parent CIM name, it is assumed that both parent CIM
218-
// and newly created CIM are present in the same directory.
219-
parentCim = filepath.Base(parentLayerCimPaths[0])
220-
}
221-
222-
cim, err := cimfs.Create(filepath.Dir(cimPath), parentCim, filepath.Base(cimPath))
223-
if err != nil {
224-
return nil, fmt.Errorf("error in creating a new cim: %w", err)
225-
}
226-
227-
sfw, err := newStdFileWriter(layerPath, parentLayerPaths)
228-
if err != nil {
229-
return nil, fmt.Errorf("error in creating new standard file writer: %w", err)
230-
}
231-
return &CimLayerWriter{
232-
ctx: ctx,
233-
s: span,
234-
layerPath: layerPath,
235-
parentLayerPaths: parentLayerPaths,
236-
cimWriter: cim,
237-
stdFileWriter: sfw,
238-
}, nil
239-
}

internal/wclayer/cim/file_writer.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,5 +86,8 @@ func (sfw *stdFileWriter) Close(ctx context.Context) error {
8686
if err := sfw.closeActiveFile(); err != nil {
8787
return fmt.Errorf("failed to close active file %s : %w", sfw.activeFile.Name(), err)
8888
}
89+
if err := sfw.root.Close(); err != nil {
90+
return fmt.Errorf("failed to close root dir: %s", err)
91+
}
8992
return nil
9093
}

0 commit comments

Comments
 (0)