Skip to content

Commit 20c7227

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. Signed-off-by: Amit Barve <[email protected]>
1 parent 01f38f8 commit 20c7227

File tree

7 files changed

+342
-95
lines changed

7 files changed

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

0 commit comments

Comments
 (0)