Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update cimfs snapshotter & differ for new hcsshim interface #10033

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ require (
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24
github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0
github.com/Microsoft/go-winio v0.6.2
github.com/Microsoft/hcsshim v0.12.6
github.com/Microsoft/hcsshim v0.13.0-rc.1
github.com/checkpoint-restore/checkpointctl v1.2.1
github.com/checkpoint-restore/go-criu/v7 v7.1.0
github.com/containerd/btrfs/v2 v2.0.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0/go.mod
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/Microsoft/hcsshim v0.12.6 h1:qEnZjoHXv+4/s0LmKZWE0/AiZmMWEIkFfWBSf1a0wlU=
github.com/Microsoft/hcsshim v0.12.6/go.mod h1:ZABCLVcvLMjIkzr9rUGcQ1QA0p0P3Ps+d3N1g2DsFfk=
github.com/Microsoft/hcsshim v0.13.0-rc.1 h1:wk/G3/qrB400SrNQUlm9TI0NHtvHIO79g1iQZ3SXQG0=
github.com/Microsoft/hcsshim v0.13.0-rc.1/go.mod h1:ZxQw6kFCqOspCZIDgJvaXUm7sBioF/lyZUwMtGqie8g=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
Expand Down
10 changes: 6 additions & 4 deletions pkg/archive/tar_opts_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,17 @@ func WithParentLayers(p []string) WriteDiffOpt {
}
}

func applyWindowsCimLayer(ctx context.Context, root string, r io.Reader, options ApplyOptions) (size int64, err error) {
return ocicimlayer.ImportCimLayerFromTar(ctx, r, root, options.Parents)
func applyWindowsCimLayer(cimPath string, parentLayerCimPaths []string) func(context.Context, string, io.Reader, ApplyOptions) (int64, error) {
return func(ctx context.Context, root string, r io.Reader, options ApplyOptions) (int64, error) {
return ocicimlayer.ImportCimLayerFromTar(ctx, r, root, cimPath, options.Parents, parentLayerCimPaths)
}
}

// AsCimContainerLayer indicates that the tar stream to apply is that of a Windows container Layer written in
// the cim format.
func AsCimContainerLayer() ApplyOpt {
func AsCimContainerLayer(cimPath string, parentLayerCimPaths []string) ApplyOpt {
return func(options *ApplyOptions) error {
options.applyFunc = applyWindowsCimLayer
options.applyFunc = applyWindowsCimLayer(cimPath, parentLayerCimPaths)
return nil
}
}
39 changes: 18 additions & 21 deletions plugins/diff/windows/cimfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"github.com/containerd/containerd/v2/core/mount"
"github.com/containerd/containerd/v2/pkg/archive"
"github.com/containerd/containerd/v2/plugins"
winsn "github.com/containerd/containerd/v2/plugins/snapshots/windows"
"github.com/containerd/errdefs"
"github.com/containerd/platforms"
"github.com/containerd/plugin"
Expand Down Expand Up @@ -77,11 +78,26 @@ func NewCimDiff(store content.Store) (CompareApplier, error) {
// provided mounts. Archive content will be extracted and decompressed if
// necessary.
func (c cimDiff) Apply(ctx context.Context, desc ocispec.Descriptor, mounts []mount.Mount, opts ...diff.ApplyOpt) (d ocispec.Descriptor, err error) {
layer, parentLayerPaths, err := cimMountsToLayerAndParents(mounts)
if len(mounts) != 1 {
return emptyDesc, fmt.Errorf("number of mounts should always be 1 for CimFS layers: %w", errdefs.ErrInvalidArgument)
} else if mounts[0].Type != "CimFS" {
ambarve marked this conversation as resolved.
Show resolved Hide resolved
return emptyDesc, fmt.Errorf("cimDiff does not support layer type %s: %w", mounts[0].Type, errdefs.ErrNotImplemented)
}

m := mounts[0]
parentLayerPaths, err := m.GetParentPaths()
if err != nil {
return emptyDesc, err
}
parentLayerCimPaths, err := winsn.GetParentCimPaths(&m)
if err != nil {
return emptyDesc, err
}
cimPath, err := winsn.GetCimPath(&m)
if err != nil {
return emptyDesc, err
}
return applyDiffCommon(ctx, c.store, desc, layer, parentLayerPaths, archive.AsCimContainerLayer(), opts...)
return applyDiffCommon(ctx, c.store, desc, m.Source, parentLayerPaths, archive.AsCimContainerLayer(cimPath, parentLayerCimPaths), opts...)
}

// Compare creates a diff between the given mounts and uploads the result
Expand All @@ -90,22 +106,3 @@ func (c cimDiff) Compare(ctx context.Context, lower, upper []mount.Mount, opts .
// support for generating layer diff of cimfs layers will be added later.
return emptyDesc, errdefs.ErrNotImplemented
}

func cimMountsToLayerAndParents(mounts []mount.Mount) (string, []string, error) {
if len(mounts) != 1 {
return "", nil, fmt.Errorf("%w: number of mounts should always be 1 for Windows layers", errdefs.ErrInvalidArgument)
}
mnt := mounts[0]
if mnt.Type != "CimFS" {
// This is a special case error. When this is received the diff service
// will attempt the next differ in the chain.
return "", nil, errdefs.ErrNotImplemented
}

parentLayerPaths, err := mnt.GetParentPaths()
if err != nil {
return "", nil, err
}

return mnt.Source, parentLayerPaths, nil
}
122 changes: 86 additions & 36 deletions plugins/snapshots/windows/cimfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ import (
"github.com/Microsoft/hcsshim"
"github.com/Microsoft/hcsshim/computestorage"
"github.com/Microsoft/hcsshim/pkg/cimfs"
cimlayer "github.com/Microsoft/hcsshim/pkg/ociwclayer/cim"
"github.com/containerd/containerd/v2/core/mount"
"github.com/containerd/containerd/v2/core/snapshots"
"github.com/containerd/containerd/v2/core/snapshots/storage"
Expand Down Expand Up @@ -95,35 +94,29 @@ func NewCimFSSnapshotter(root string) (snapshots.Snapshotter, error) {
return nil, fmt.Errorf("failed to init base scratch VHD: %w", err)
}

if err = os.MkdirAll(filepath.Join(baseSn.info.HomeDir, "cim-layers"), 0755); err != nil {
return nil, err
}

return &cimFSSnapshotter{
windowsBaseSnapshotter: baseSn,
cimDir: filepath.Join(baseSn.info.HomeDir, "cim-layers"),
}, nil
}

// getCimLayerPath returns the path of the cim file for the given snapshot. Note that this function doesn't
// getLayerCimPath returns the path of the cim file for the given snapshot. Note that this function doesn't
// actually check if the cim layer exists it simply does string manipulation to generate the path isCimLayer
// can be used to verify if it is actually a cim layer.
func getCimLayerPath(cimDir, snID string) string {
return filepath.Join(cimDir, (snID + ".cim"))
func (s *cimFSSnapshotter) getLayerCimPath(snID string) string {
return filepath.Join(s.cimDir, (snID + ".cim"))
}

// isCimLayer checks if the snapshot referred by the given key is actually a cim layer. With CimFS
// snapshotter all the read-only (i.e image) layers are stored in the cim format while we still use VHDs for
// scratch layers.
func (s *cimFSSnapshotter) isCimLayer(ctx context.Context, key string) (bool, error) {
id, _, _, err := storage.GetInfo(ctx, key)
if err != nil {
return false, fmt.Errorf("get snapshot info: %w", err)
func (s *cimFSSnapshotter) parentIDsToCimPaths(parentIDs []string) []string {
cimPaths := make([]string, 0, len(parentIDs))
for _, id := range parentIDs {
cimPaths = append(cimPaths, s.getLayerCimPath(id))
}
snCimPath := getCimLayerPath(s.cimDir, id)
if _, err := os.Stat(snCimPath); err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
return true, nil
return cimPaths
}

func (s *cimFSSnapshotter) Usage(ctx context.Context, key string) (snapshots.Usage, error) {
Expand All @@ -138,15 +131,14 @@ func (s *cimFSSnapshotter) Usage(ctx context.Context, key string) (snapshots.Usa
}
defer t.Rollback()

id, _, _, err := storage.GetInfo(ctx, key)
id, info, _, err := storage.GetInfo(ctx, key)
if err != nil {
return snapshots.Usage{}, fmt.Errorf("failed to get snapshot info: %w", err)
}

if ok, err := s.isCimLayer(ctx, key); err != nil {
return snapshots.Usage{}, err
} else if ok {
cimUsage, err := cimfs.GetCimUsage(ctx, getCimLayerPath(s.cimDir, id))
if info.Kind == snapshots.KindCommitted {
// Committed MUST be a cimfs layer
kevpar marked this conversation as resolved.
Show resolved Hide resolved
cimUsage, err := cimfs.GetCimUsage(ctx, s.getLayerCimPath(id))
if err != nil {
return snapshots.Usage{}, err
}
Expand Down Expand Up @@ -219,7 +211,7 @@ func (s *cimFSSnapshotter) Remove(ctx context.Context, key string) error {
return fmt.Errorf("%w: %s", errdefs.ErrFailedPrecondition, err)
}

if err := cimlayer.DestroyCimLayer(s.getSnapshotDir(ID)); err != nil {
if err := cimfs.DestroyCim(ctx, s.getLayerCimPath(ID)); err != nil {
// Must be cleaned up, any "rm-*" could be removed if no active transactions
log.G(ctx).WithError(err).WithField("ID", ID).Warnf("failed to cleanup cim files")
}
Expand Down Expand Up @@ -325,25 +317,31 @@ func (s *cimFSSnapshotter) mounts(sn storage.Snapshot, key string) []mount.Mount
roFlag = "rw"
}

source := s.getSnapshotDir(sn.ID)
parentLayerPaths := s.parentIDsToParentPaths(sn.ParentIDs)

mountType := "CimFS"

// error is not checked here, as a string array will never fail to Marshal
parentLayersJSON, _ := json.Marshal(parentLayerPaths)
parentLayersOption := mount.ParentLayerPathsFlag + string(parentLayersJSON)

options := []string{
roFlag,
}

// add the layer CIM path - this path will only be used if we are extracting an
// image layer, in case of scratch snapshots this path MUST be ignored.
layerCimPath := s.getLayerCimPath(sn.ID)
options = append(options, LayerCimPathFlag+layerCimPath)

if len(sn.ParentIDs) != 0 {
parentLayerPaths := s.parentIDsToParentPaths(sn.ParentIDs)
parentLayerCimPaths := s.parentIDsToCimPaths(sn.ParentIDs)
// error is not checked here, as a string array will never fail to Marshal
parentLayersJSON, _ := json.Marshal(parentLayerPaths)
parentLayersOption := mount.ParentLayerPathsFlag + string(parentLayersJSON)
parentCimLayersJSON, _ := json.Marshal(parentLayerCimPaths)
parentCimLayersOption := ParentLayerCimPathsFlag + string(parentCimLayersJSON)
options = append(options, parentLayersOption)
options = append(options, parentCimLayersOption)
}

mounts := []mount.Mount{
{
Source: source,
Type: mountType,
Source: s.getSnapshotDir(sn.ID),
Type: "CimFS",
Options: options,
},
}
Expand Down Expand Up @@ -426,3 +424,55 @@ func createScratchVHDs(ctx context.Context, path string) (err error) {
}
return nil
}

const (
// LayerCimPathFlag is the option flag used to represent the path at which a layer CIM must be stored. This
// flag is only included if an image layer is being extracted onto the snapshot i.e the snapshot key has an
// UnpackKeyPrefix.
LayerCimPathFlag = "cimpath="

// Similar to ParentLayerPathsFlag this is the optinos flag used to represent the JSON encoded list of
// parent layer CIMs
ParentLayerCimPathsFlag = "parentCimPaths="
)

// getOptionByPrefix finds an option that has the provided prefix, cuts the prefix from
// that option string and return the remaining string. Boolean return is set to true if an
// option is found with the given prefix. It is set to false otherwise.
func getOptionByPrefix(m *mount.Mount, prefix string) (string, bool) {
for _, option := range m.Options {
val, found := strings.CutPrefix(option, prefix)
if found {
return val, true
}
}
return "", false
}

// gets the paths of the parent cims of this mount
func GetParentCimPaths(m *mount.Mount) ([]string, error) {
if m.Type != "CimFS" {
return nil, fmt.Errorf("invalid mount type: '%s'", m.Type)
}
var parentCimPaths []string
val, found := getOptionByPrefix(m, ParentLayerCimPathsFlag)
if !found {
return parentCimPaths, nil
}
err := json.Unmarshal([]byte(val), &parentCimPaths)
return parentCimPaths, err
}

// Only applies to a snapshot created for image extraction, for such a snapshot provides the
// path to a cim in which image layer will be extracted.
func GetCimPath(m *mount.Mount) (string, error) {
if m.Type != "CimFS" {
return "", fmt.Errorf("invalid mount type: '%s'", m.Type)
}
cimPath, found := getOptionByPrefix(m, LayerCimPathFlag)
if !found {
return "", fmt.Errorf("cim path not found")
}
return cimPath, nil

}
58 changes: 58 additions & 0 deletions plugins/snapshots/windows/cimfs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//go:build windows
// +build windows

/*
Copyright The containerd Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package windows

import (
"encoding/json"
"testing"

"github.com/containerd/containerd/v2/core/mount"
)

func TestGetOptionByPrefix(t *testing.T) {
cimPath := "C:\\fake\\cim\\path.cim"
cimPathOpt := LayerCimPathFlag + cimPath
parentCimPaths := []string{"C:\\fake\\parent1.cim", "C:\\fake\\parent2.cim"}
parentCimLayersJSON, _ := json.Marshal(parentCimPaths)
parentCimLayersOpt := ParentLayerCimPathsFlag + string(parentCimLayersJSON)
m := &mount.Mount{
Source: "C:\\foo\\bar",
Type: "CimFS",
Options: []string{cimPathOpt, parentCimLayersOpt},
}

parsedCimPath, err := GetCimPath(m)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if cimPath != parsedCimPath {
t.Errorf("Expected `%s`, got `%s`", cimPath, parsedCimPath)
}

parsedParentCimPaths, err := GetParentCimPaths(m)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if len(parsedParentCimPaths) != 2 ||
parsedParentCimPaths[0] != parentCimPaths[0] ||
parsedParentCimPaths[1] != parentCimPaths[1] {
t.Errorf("Expected `%v`, got `%v`", parentCimPaths, parsedParentCimPaths)
}
}
2 changes: 1 addition & 1 deletion script/setup/runhcs-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v0.12.6
v0.13.0-rc.1
Loading
Loading