Add cimfs differ and snapshotter

Details about CimFs project are discussed in #8346

Signed-off-by: Amit Barve <ambarve@microsoft.com>
This commit is contained in:
Amit Barve
2023-09-14 16:18:13 -07:00
parent 643fa70a7d
commit daa1ea522b
104 changed files with 3848 additions and 2996 deletions

View File

@@ -0,0 +1,291 @@
//go:build windows
// +build windows
package cimfs
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"unsafe"
"github.com/Microsoft/go-winio"
"github.com/Microsoft/hcsshim/internal/log"
"github.com/Microsoft/hcsshim/internal/winapi"
"github.com/sirupsen/logrus"
"golang.org/x/sys/windows"
)
// CimFsWriter represents a writer to a single CimFS filesystem instance. On disk, the
// image is composed of a filesystem file and several object ID and region files.
// Note: The CimFsWriter isn't thread safe!
type CimFsWriter struct {
// name of this cim. Usually a <name>.cim file will be created to represent this cim.
name string
// handle is the CIMFS_IMAGE_HANDLE that must be passed when calling CIMFS APIs.
handle winapi.FsHandle
// name of the active file i.e the file to which we are currently writing.
activeName string
// stream to currently active file.
activeStream winapi.StreamHandle
// amount of bytes that can be written to the activeStream.
activeLeft uint64
}
// Create creates a new cim image. The CimFsWriter returned can then be used to do
// operations on this cim.
func Create(imagePath string, oldFSName string, newFSName string) (_ *CimFsWriter, err error) {
var oldNameBytes *uint16
// CimCreateImage API call has different behavior if the value of oldNameBytes / newNameBytes
// is empty than if it is nil. So we have to convert those strings into *uint16 here.
fsName := oldFSName
if oldFSName != "" {
oldNameBytes, err = windows.UTF16PtrFromString(oldFSName)
if err != nil {
return nil, err
}
}
var newNameBytes *uint16
if newFSName != "" {
fsName = newFSName
newNameBytes, err = windows.UTF16PtrFromString(newFSName)
if err != nil {
return nil, err
}
}
var handle winapi.FsHandle
if err := winapi.CimCreateImage(imagePath, oldNameBytes, newNameBytes, &handle); err != nil {
return nil, fmt.Errorf("failed to create cim image at path %s, oldName: %s, newName: %s: %w", imagePath, oldFSName, newFSName, err)
}
return &CimFsWriter{handle: handle, name: filepath.Join(imagePath, fsName)}, nil
}
// CreateAlternateStream creates alternate stream of given size at the given path inside the cim. This will
// replace the current active stream. Always, finish writing current active stream and then create an
// alternate stream.
func (c *CimFsWriter) CreateAlternateStream(path string, size uint64) (err error) {
err = c.closeStream()
if err != nil {
return err
}
err = winapi.CimCreateAlternateStream(c.handle, path, size, &c.activeStream)
if err != nil {
return fmt.Errorf("failed to create alternate stream for path %s: %w", path, err)
}
c.activeName = path
return nil
}
// closes the currently active stream.
func (c *CimFsWriter) closeStream() error {
if c.activeStream == 0 {
return nil
}
err := winapi.CimCloseStream(c.activeStream)
if err == nil && c.activeLeft > 0 {
// Validate here because CimCloseStream does not and this improves error
// reporting. Otherwise the error will occur in the context of
// cimWriteStream.
err = fmt.Errorf("incomplete write, %d bytes left in the stream %s", c.activeLeft, c.activeName)
}
if err != nil {
err = &PathError{Cim: c.name, Op: "closeStream", Path: c.activeName, Err: err}
}
c.activeLeft = 0
c.activeStream = 0
c.activeName = ""
return err
}
// AddFile adds a new file to the image. The file is added at the specified path. After
// calling this function, the file is set as the active stream for the image, so data can
// be written by calling `Write`.
func (c *CimFsWriter) AddFile(path string, info *winio.FileBasicInfo, fileSize int64, securityDescriptor []byte, extendedAttributes []byte, reparseData []byte) error {
err := c.closeStream()
if err != nil {
return err
}
fileMetadata := &winapi.CimFsFileMetadata{
Attributes: info.FileAttributes,
FileSize: fileSize,
CreationTime: info.CreationTime,
LastWriteTime: info.LastWriteTime,
ChangeTime: info.ChangeTime,
LastAccessTime: info.LastAccessTime,
}
if len(securityDescriptor) == 0 {
// Passing an empty security descriptor creates a CIM in a weird state.
// Pass the NULL DACL.
securityDescriptor = nullSd
}
fileMetadata.SecurityDescriptorBuffer = unsafe.Pointer(&securityDescriptor[0])
fileMetadata.SecurityDescriptorSize = uint32(len(securityDescriptor))
if len(reparseData) > 0 {
fileMetadata.ReparseDataBuffer = unsafe.Pointer(&reparseData[0])
fileMetadata.ReparseDataSize = uint32(len(reparseData))
}
if len(extendedAttributes) > 0 {
fileMetadata.ExtendedAttributes = unsafe.Pointer(&extendedAttributes[0])
fileMetadata.EACount = uint32(len(extendedAttributes))
}
// remove the trailing `\` if present, otherwise it trips off the cim writer
path = strings.TrimSuffix(path, "\\")
err = winapi.CimCreateFile(c.handle, path, fileMetadata, &c.activeStream)
if err != nil {
return &PathError{Cim: c.name, Op: "addFile", Path: path, Err: err}
}
c.activeName = path
if info.FileAttributes&(windows.FILE_ATTRIBUTE_DIRECTORY) == 0 {
c.activeLeft = uint64(fileSize)
}
return nil
}
// Write writes bytes to the active stream.
func (c *CimFsWriter) Write(p []byte) (int, error) {
if c.activeStream == 0 {
return 0, fmt.Errorf("no active stream")
}
if uint64(len(p)) > c.activeLeft {
return 0, &PathError{Cim: c.name, Op: "write", Path: c.activeName, Err: fmt.Errorf("wrote too much")}
}
err := winapi.CimWriteStream(c.activeStream, uintptr(unsafe.Pointer(&p[0])), uint32(len(p)))
if err != nil {
err = &PathError{Cim: c.name, Op: "write", Path: c.activeName, Err: err}
return 0, err
}
c.activeLeft -= uint64(len(p))
return len(p), nil
}
// AddLink adds a hard link from `oldPath` to `newPath` in the image.
func (c *CimFsWriter) AddLink(oldPath string, newPath string) error {
err := c.closeStream()
if err != nil {
return err
}
err = winapi.CimCreateHardLink(c.handle, newPath, oldPath)
if err != nil {
err = &LinkError{Cim: c.name, Op: "addLink", Old: oldPath, New: newPath, Err: err}
}
return err
}
// Unlink deletes the file at `path` from the image.
func (c *CimFsWriter) Unlink(path string) error {
err := c.closeStream()
if err != nil {
return err
}
//TODO(ambarve): CimDeletePath currently returns an error if the file isn't found but we ideally want
// to put a tombstone at that path so that when cims are merged it removes that file from the lower
// layer
err = winapi.CimDeletePath(c.handle, path)
if err != nil && !os.IsNotExist(err) {
err = &PathError{Cim: c.name, Op: "unlink", Path: path, Err: err}
return err
}
return nil
}
func (c *CimFsWriter) commit() error {
err := c.closeStream()
if err != nil {
return err
}
err = winapi.CimCommitImage(c.handle)
if err != nil {
err = &OpError{Cim: c.name, Op: "commit", Err: err}
}
return err
}
// Close closes the CimFS filesystem.
func (c *CimFsWriter) Close() error {
if c.handle == 0 {
return fmt.Errorf("invalid writer")
}
if err := c.commit(); err != nil {
return &OpError{Cim: c.name, Op: "commit", Err: err}
}
if err := winapi.CimCloseImage(c.handle); err != nil {
return &OpError{Cim: c.name, Op: "close", Err: err}
}
c.handle = 0
return nil
}
// DestroyCim finds out the region files, object files of this cim and then delete
// the region files, object files and the <layer-id>.cim file itself.
func DestroyCim(ctx context.Context, cimPath string) (retErr error) {
regionFilePaths, err := getRegionFilePaths(ctx, cimPath)
if err != nil {
log.G(ctx).WithError(err).Warnf("get region files for cim %s", cimPath)
if retErr == nil { //nolint:govet // nilness: consistency with below
retErr = err
}
}
objectFilePaths, err := getObjectIDFilePaths(ctx, cimPath)
if err != nil {
log.G(ctx).WithError(err).Warnf("get objectid file for cim %s", cimPath)
if retErr == nil {
retErr = err
}
}
log.G(ctx).WithFields(logrus.Fields{
"cimPath": cimPath,
"regionFiles": regionFilePaths,
"objectFiles": objectFilePaths,
}).Debug("destroy cim")
for _, regFilePath := range regionFilePaths {
if err := os.Remove(regFilePath); err != nil {
log.G(ctx).WithError(err).Warnf("remove file %s", regFilePath)
if retErr == nil {
retErr = err
}
}
}
for _, objFilePath := range objectFilePaths {
if err := os.Remove(objFilePath); err != nil {
log.G(ctx).WithError(err).Warnf("remove file %s", objFilePath)
if retErr == nil {
retErr = err
}
}
}
if err := os.Remove(cimPath); err != nil {
log.G(ctx).WithError(err).Warnf("remove file %s", cimPath)
if retErr == nil {
retErr = err
}
}
return retErr
}
// GetCimUsage returns the total disk usage in bytes by the cim at path `cimPath`.
func GetCimUsage(ctx context.Context, cimPath string) (uint64, error) {
regionFilePaths, err := getRegionFilePaths(ctx, cimPath)
if err != nil {
return 0, fmt.Errorf("get region file paths for cim %s: %w", cimPath, err)
}
objectFilePaths, err := getObjectIDFilePaths(ctx, cimPath)
if err != nil {
return 0, fmt.Errorf("get objectid file for cim %s: %w", cimPath, err)
}
var totalUsage uint64
for _, f := range append(regionFilePaths, objectFilePaths...) {
fi, err := os.Stat(f)
if err != nil {
return 0, fmt.Errorf("stat file %s: %w", f, err)
}
totalUsage += uint64(fi.Size())
}
return totalUsage, nil
}

17
vendor/github.com/Microsoft/hcsshim/pkg/cimfs/cimfs.go generated vendored Normal file
View File

@@ -0,0 +1,17 @@
//go:build windows
// +build windows
package cimfs
import (
"github.com/Microsoft/hcsshim/osversion"
"github.com/sirupsen/logrus"
)
func IsCimFSSupported() bool {
rv, err := osversion.BuildRevision()
if err != nil {
logrus.WithError(err).Warn("get build revision")
}
return osversion.Build() == 20348 && rv >= 2031
}

134
vendor/github.com/Microsoft/hcsshim/pkg/cimfs/common.go generated vendored Normal file
View File

@@ -0,0 +1,134 @@
//go:build windows
// +build windows
package cimfs
import (
"bytes"
"context"
"encoding/binary"
"fmt"
"os"
"path/filepath"
"github.com/Microsoft/hcsshim/internal/log"
"github.com/Microsoft/hcsshim/pkg/cimfs/format"
)
var (
// Equivalent to SDDL of "D:NO_ACCESS_CONTROL".
nullSd = []byte{1, 0, 4, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
)
type OpError struct {
Cim string
Op string
Err error
}
func (e *OpError) Error() string {
s := "cim " + e.Op + " " + e.Cim
s += ": " + e.Err.Error()
return s
}
// PathError is the error type returned by most functions in this package.
type PathError struct {
Cim string
Op string
Path string
Err error
}
func (e *PathError) Error() string {
s := "cim " + e.Op + " " + e.Cim
s += ":" + e.Path
s += ": " + e.Err.Error()
return s
}
type LinkError struct {
Cim string
Op string
Old string
New string
Err error
}
func (e *LinkError) Error() string {
return "cim " + e.Op + " " + e.Old + " " + e.New + ": " + e.Err.Error()
}
func validateHeader(h *format.CommonHeader) error {
if !bytes.Equal(h.Magic[:], format.MagicValue[:]) {
return fmt.Errorf("not a cim file")
}
if h.Version.Major > format.CurrentVersion.Major || h.Version.Major < format.MinSupportedVersion.Major {
return fmt.Errorf("unsupported cim version. cim version %v must be between %v & %v", h.Version, format.MinSupportedVersion, format.CurrentVersion)
}
return nil
}
func readFilesystemHeader(f *os.File) (format.FilesystemHeader, error) {
var fsh format.FilesystemHeader
if err := binary.Read(f, binary.LittleEndian, &fsh); err != nil {
return fsh, fmt.Errorf("reading filesystem header: %w", err)
}
if err := validateHeader(&fsh.Common); err != nil {
return fsh, fmt.Errorf("validating filesystem header: %w", err)
}
return fsh, nil
}
// Returns the paths of all the objectID files associated with the cim at `cimPath`.
func getObjectIDFilePaths(ctx context.Context, cimPath string) ([]string, error) {
f, err := os.Open(cimPath)
if err != nil {
return []string{}, fmt.Errorf("open cim file %s: %w", cimPath, err)
}
defer f.Close()
fsh, err := readFilesystemHeader(f)
if err != nil {
return []string{}, fmt.Errorf("readingp cim header: %w", err)
}
paths := []string{}
for i := 0; i < int(fsh.Regions.Count); i++ {
path := filepath.Join(filepath.Dir(cimPath), fmt.Sprintf("%s_%v_%d", format.ObjectIDFileName, fsh.Regions.ID, i))
if _, err := os.Stat(path); err == nil {
paths = append(paths, path)
} else {
log.G(ctx).WithError(err).Warnf("stat for object file %s", path)
}
}
return paths, nil
}
// Returns the paths of all the region files associated with the cim at `cimPath`.
func getRegionFilePaths(ctx context.Context, cimPath string) ([]string, error) {
f, err := os.Open(cimPath)
if err != nil {
return []string{}, fmt.Errorf("open cim file %s: %w", cimPath, err)
}
defer f.Close()
fsh, err := readFilesystemHeader(f)
if err != nil {
return []string{}, fmt.Errorf("reading cim header: %w", err)
}
paths := []string{}
for i := 0; i < int(fsh.Regions.Count); i++ {
path := filepath.Join(filepath.Dir(cimPath), fmt.Sprintf("%s_%v_%d", format.RegionFileName, fsh.Regions.ID, i))
if _, err := os.Stat(path); err == nil {
paths = append(paths, path)
} else {
log.G(ctx).WithError(err).Warnf("stat for region file %s", path)
}
}
return paths, nil
}

3
vendor/github.com/Microsoft/hcsshim/pkg/cimfs/doc.go generated vendored Normal file
View File

@@ -0,0 +1,3 @@
// This package provides simple go wrappers on top of the win32 CIMFS mount APIs.
// The mounting/unmount of cim layers is done by the cim mount functions the internal/wclayer/cim package.
package cimfs

View File

@@ -0,0 +1,4 @@
// format package maintains some basic structures to allows us to read header of a cim file. This is mostly
// required to understand the region & objectid files associated with a particular cim. Otherwise, we don't
// need to parse the cim format.
package format

View File

@@ -0,0 +1,61 @@
//go:build windows
// +build windows
package format
import "github.com/Microsoft/go-winio/pkg/guid"
const (
RegionFileName = "region"
ObjectIDFileName = "objectid"
)
// Magic specifies the magic number at the beginning of a file.
type Magic [8]uint8
var MagicValue = Magic([8]uint8{'c', 'i', 'm', 'f', 'i', 'l', 'e', '0'})
type Version struct {
Major, Minor uint32
}
var CurrentVersion = Version{3, 0}
var MinSupportedVersion = Version{2, 0}
type FileType uint8
// RegionOffset encodes an offset to objects as index of the region file
// containing the object and the byte offset within that file.
type RegionOffset uint64
// CommonHeader is the common header for all CIM-related files.
type CommonHeader struct {
Magic Magic
HeaderLength uint32
Type FileType
Reserved uint8
Reserved2 uint16
Version Version
Reserved3 uint64
}
type RegionSet struct {
ID guid.GUID
Count uint16
Reserved uint16
Reserved1 uint32
}
// FilesystemHeader is the header for a filesystem file.
//
// The filesystem file points to the filesystem object inside a region
// file and specifies regions sets.
type FilesystemHeader struct {
Common CommonHeader
Regions RegionSet
FilesystemOffset RegionOffset
Reserved uint32
Reserved1 uint16
ParentCount uint16
}

View File

@@ -0,0 +1,65 @@
//go:build windows
// +build windows
package cimfs
import (
"fmt"
"path/filepath"
"strings"
"github.com/Microsoft/go-winio/pkg/guid"
"github.com/Microsoft/hcsshim/internal/winapi"
"github.com/pkg/errors"
)
type MountError struct {
Cim string
Op string
VolumeGUID guid.GUID
Err error
}
func (e *MountError) Error() string {
s := "cim " + e.Op
if e.Cim != "" {
s += " " + e.Cim
}
s += " " + e.VolumeGUID.String() + ": " + e.Err.Error()
return s
}
// Mount mounts the given cim at a volume with given GUID. Returns the full volume
// path if mount is successful.
func Mount(cimPath string, volumeGUID guid.GUID, mountFlags uint32) (string, error) {
if err := winapi.CimMountImage(filepath.Dir(cimPath), filepath.Base(cimPath), mountFlags, &volumeGUID); err != nil {
return "", &MountError{Cim: cimPath, Op: "Mount", VolumeGUID: volumeGUID, Err: err}
}
return fmt.Sprintf("\\\\?\\Volume{%s}\\", volumeGUID.String()), nil
}
// Unmount unmounts the cim at mounted at path `volumePath`.
func Unmount(volumePath string) error {
// The path is expected to be in the \\?\Volume{GUID}\ format
if volumePath[len(volumePath)-1] != '\\' {
volumePath += "\\"
}
if !(strings.HasPrefix(volumePath, "\\\\?\\Volume{") && strings.HasSuffix(volumePath, "}\\")) {
return errors.Errorf("volume path %s is not in the expected format", volumePath)
}
trimmedStr := strings.TrimPrefix(volumePath, "\\\\?\\Volume{")
trimmedStr = strings.TrimSuffix(trimmedStr, "}\\")
volGUID, err := guid.FromString(trimmedStr)
if err != nil {
return errors.Wrapf(err, "guid parsing failed for %s", trimmedStr)
}
if err := winapi.CimDismountImage(&volGUID); err != nil {
return &MountError{VolumeGUID: volGUID, Op: "Unmount", Err: err}
}
return nil
}

View File

@@ -89,7 +89,7 @@ func putBuf(b *bytes.Buffer) {
bytesBufferPool.Put(b)
}
// Runhcs is the client to the runhcs cli
// Runhcs is the client to the runhcs cli.
type Runhcs struct {
// Debug enables debug output for logging.
Debug bool
@@ -130,8 +130,8 @@ func (r *Runhcs) args() []string {
return out
}
func (r *Runhcs) command(context context.Context, args ...string) *exec.Cmd {
cmd := exec.CommandContext(context, getCommandPath(), append(r.args(), args...)...)
func (r *Runhcs) command(ctx context.Context, args ...string) *exec.Cmd {
cmd := exec.CommandContext(ctx, getCommandPath(), append(r.args(), args...)...)
cmd.Env = os.Environ()
return cmd
}
@@ -139,7 +139,7 @@ func (r *Runhcs) command(context context.Context, args ...string) *exec.Cmd {
// runOrError will run the provided command. If an error is
// encountered and neither Stdout or Stderr was set the error and the
// stderr of the command will be returned in the format of <error>:
// <stderr>
// <stderr>.
func (r *Runhcs) runOrError(cmd *exec.Cmd) error {
if cmd.Stdout != nil || cmd.Stderr != nil {
ec, err := runc.Monitor.Start(cmd)
@@ -154,7 +154,7 @@ func (r *Runhcs) runOrError(cmd *exec.Cmd) error {
}
data, err := cmdOutput(cmd, true)
if err != nil {
return fmt.Errorf("%s: %s", err, data)
return fmt.Errorf("%s: %s", err, data) //nolint:errorlint // legacy code
}
return nil
}

View File

@@ -10,8 +10,8 @@ import (
)
// CreateScratch creates a scratch vhdx at 'destpath' that is ext4 formatted.
func (r *Runhcs) CreateScratch(context context.Context, destpath string) error {
return r.CreateScratchWithOpts(context, destpath, nil)
func (r *Runhcs) CreateScratch(ctx context.Context, destpath string) error {
return r.CreateScratchWithOpts(ctx, destpath, nil)
}
// CreateScratchOpts is the set of options that can be used with the
@@ -43,7 +43,7 @@ func (opt *CreateScratchOpts) args() ([]string, error) {
// CreateScratchWithOpts creates a scratch vhdx at 'destpath' that is ext4
// formatted based on `opts`.
func (r *Runhcs) CreateScratchWithOpts(context context.Context, destpath string, opts *CreateScratchOpts) error {
func (r *Runhcs) CreateScratchWithOpts(ctx context.Context, destpath string, opts *CreateScratchOpts) error {
args := []string{"create-scratch", "--destpath", destpath}
if opts != nil {
oargs, err := opts.args()
@@ -52,5 +52,5 @@ func (r *Runhcs) CreateScratchWithOpts(context context.Context, destpath string,
}
args = append(args, oargs...)
}
return r.runOrError(r.command(context, args...))
return r.runOrError(r.command(ctx, args...))
}

View File

@@ -64,7 +64,7 @@ func (opt *CreateOpts) args() ([]string, error) {
// Create creates a new container and returns its pid if it was created
// successfully.
func (r *Runhcs) Create(context context.Context, id, bundle string, opts *CreateOpts) error {
func (r *Runhcs) Create(ctx context.Context, id, bundle string, opts *CreateOpts) error {
args := []string{"create", "--bundle", bundle}
if opts != nil {
oargs, err := opts.args()
@@ -73,14 +73,14 @@ func (r *Runhcs) Create(context context.Context, id, bundle string, opts *Create
}
args = append(args, oargs...)
}
cmd := r.command(context, append(args, id)...)
cmd := r.command(ctx, append(args, id)...)
if opts != nil && opts.IO != nil {
opts.Set(cmd)
}
if cmd.Stdout == nil && cmd.Stderr == nil {
data, err := cmdOutput(cmd, true)
if err != nil {
return fmt.Errorf("%s: %s", err, data)
return fmt.Errorf("%s: %s", err, data) //nolint:errorlint // legacy code
}
return nil
}

View File

@@ -22,7 +22,7 @@ func (opt *DeleteOpts) args() ([]string, error) {
// Delete any resources held by the container often used with detached
// containers.
func (r *Runhcs) Delete(context context.Context, id string, opts *DeleteOpts) error {
func (r *Runhcs) Delete(ctx context.Context, id string, opts *DeleteOpts) error {
args := []string{"delete"}
if opts != nil {
oargs, err := opts.args()
@@ -31,5 +31,5 @@ func (r *Runhcs) Delete(context context.Context, id string, opts *DeleteOpts) er
}
args = append(args, oargs...)
}
return r.runOrError(r.command(context, append(args, id)...))
return r.runOrError(r.command(ctx, append(args, id)...))
}

View File

@@ -51,7 +51,7 @@ func (opt *ExecOpts) args() ([]string, error) {
// Exec executes an additional process inside the container based on the
// oci.Process spec found at processFile.
func (r *Runhcs) Exec(context context.Context, id, processFile string, opts *ExecOpts) error {
func (r *Runhcs) Exec(ctx context.Context, id, processFile string, opts *ExecOpts) error {
args := []string{"exec", "--process", processFile}
if opts != nil {
oargs, err := opts.args()
@@ -60,14 +60,14 @@ func (r *Runhcs) Exec(context context.Context, id, processFile string, opts *Exe
}
args = append(args, oargs...)
}
cmd := r.command(context, append(args, id)...)
cmd := r.command(ctx, append(args, id)...)
if opts != nil && opts.IO != nil {
opts.Set(cmd)
}
if cmd.Stdout == nil && cmd.Stderr == nil {
data, err := cmdOutput(cmd, true)
if err != nil {
return fmt.Errorf("%s: %s", err, data)
return fmt.Errorf("%s: %s", err, data) //nolint:errorlint // legacy code
}
return nil
}

View File

@@ -8,6 +8,6 @@ import (
// Kill sends the specified signal (default: SIGTERM) to the container's init
// process.
func (r *Runhcs) Kill(context context.Context, id, signal string) error {
return r.runOrError(r.command(context, "kill", id, signal))
func (r *Runhcs) Kill(ctx context.Context, id, signal string) error {
return r.runOrError(r.command(ctx, "kill", id, signal))
}

View File

@@ -17,8 +17,8 @@ type ContainerState = irunhcs.ContainerState
//
// Note: This is specific to the Runhcs.Root namespace provided in the global
// settings.
func (r *Runhcs) List(context context.Context) ([]*ContainerState, error) {
data, err := cmdOutput(r.command(context, "list", "--format=json"), false)
func (r *Runhcs) List(ctx context.Context) ([]*ContainerState, error) {
data, err := cmdOutput(r.command(ctx, "list", "--format=json"), false)
if err != nil {
return nil, err
}

View File

@@ -7,6 +7,6 @@ import (
)
// Pause suspends all processes inside the container.
func (r *Runhcs) Pause(context context.Context, id string) error {
return r.runOrError(r.command(context, "pause", id))
func (r *Runhcs) Pause(ctx context.Context, id string) error {
return r.runOrError(r.command(ctx, "pause", id))
}

View File

@@ -9,10 +9,10 @@ import (
)
// Ps displays the processes running inside a container.
func (r *Runhcs) Ps(context context.Context, id string) ([]int, error) {
data, err := cmdOutput(r.command(context, "ps", "--format=json", id), true)
func (r *Runhcs) Ps(ctx context.Context, id string) ([]int, error) {
data, err := cmdOutput(r.command(ctx, "ps", "--format=json", id), true)
if err != nil {
return nil, fmt.Errorf("%s: %s", err, data)
return nil, fmt.Errorf("%s: %s", err, data) //nolint:errorlint // legacy code
}
var out []int
if err := json.Unmarshal(data, &out); err != nil {

View File

@@ -22,7 +22,7 @@ func (opt *ResizeTTYOpts) args() ([]string, error) {
}
// ResizeTTY updates the terminal size for a container process.
func (r *Runhcs) ResizeTTY(context context.Context, id string, width, height uint16, opts *ResizeTTYOpts) error {
func (r *Runhcs) ResizeTTY(ctx context.Context, id string, width, height uint16, opts *ResizeTTYOpts) error {
args := []string{"resize-tty"}
if opts != nil {
oargs, err := opts.args()
@@ -31,5 +31,5 @@ func (r *Runhcs) ResizeTTY(context context.Context, id string, width, height uin
}
args = append(args, oargs...)
}
return r.runOrError(r.command(context, append(args, id, strconv.FormatUint(uint64(width), 10), strconv.FormatUint(uint64(height), 10))...))
return r.runOrError(r.command(ctx, append(args, id, strconv.FormatUint(uint64(width), 10), strconv.FormatUint(uint64(height), 10))...))
}

View File

@@ -7,6 +7,6 @@ import (
)
// Resume resumes all processes that have been previously paused.
func (r *Runhcs) Resume(context context.Context, id string) error {
return r.runOrError(r.command(context, "resume", id))
func (r *Runhcs) Resume(ctx context.Context, id string) error {
return r.runOrError(r.command(ctx, "resume", id))
}

View File

@@ -7,6 +7,6 @@ import (
)
// Start will start an already created container.
func (r *Runhcs) Start(context context.Context, id string) error {
return r.runOrError(r.command(context, "start", id))
func (r *Runhcs) Start(ctx context.Context, id string) error {
return r.runOrError(r.command(ctx, "start", id))
}

View File

@@ -9,10 +9,10 @@ import (
)
// State outputs the state of a container.
func (r *Runhcs) State(context context.Context, id string) (*ContainerState, error) {
data, err := cmdOutput(r.command(context, "state", id), true)
func (r *Runhcs) State(ctx context.Context, id string) (*ContainerState, error) {
data, err := cmdOutput(r.command(ctx, "state", id), true)
if err != nil {
return nil, fmt.Errorf("%s: %s", err, data)
return nil, fmt.Errorf("%s: %s", err, data) //nolint:errorlint // legacy code
}
var out ContainerState
if err := json.Unmarshal(data, &out); err != nil {

View File

@@ -0,0 +1,166 @@
//go:build windows
// +build windows
package cim
import (
"archive/tar"
"bufio"
"context"
"errors"
"fmt"
"io"
"os"
"path"
"path/filepath"
"strings"
"github.com/Microsoft/go-winio/backuptar"
"github.com/Microsoft/hcsshim/internal/log"
"github.com/Microsoft/hcsshim/internal/wclayer/cim"
"github.com/Microsoft/hcsshim/pkg/ociwclayer"
"golang.org/x/sys/windows"
)
// ImportCimLayerFromTar reads a layer from an OCI layer tar stream and extracts it into
// the CIM format at the specified path. The caller must specify the parent layers, if
// any, ordered from lowest to highest layer.
// This function expects that the layer paths (both the layer that is being imported & the parent layers) are
// formatted like `.../snapshots/<id>` and the corresponding layer CIMs are located/will be created at
// `.../snapshots/cim-layers/<id>.cim`. Each CIM file also has corresponding region & objectID files and those
// files will also be stored inside the `cim-layers` directory.
//
// This function returns the total size of the layer's files, in bytes.
func ImportCimLayerFromTar(ctx context.Context, r io.Reader, layerPath string, parentLayerPaths []string) (int64, error) {
err := os.MkdirAll(layerPath, 0)
if err != nil {
return 0, err
}
w, err := cim.NewCimLayerWriter(ctx, layerPath, parentLayerPaths)
if err != nil {
return 0, err
}
n, err := writeCimLayerFromTar(ctx, r, w, layerPath)
cerr := w.Close(ctx)
if err != nil {
return 0, err
}
if cerr != nil {
return 0, cerr
}
return n, nil
}
func writeCimLayerFromTar(ctx context.Context, r io.Reader, w *cim.CimLayerWriter, layerPath string) (int64, error) {
tr := tar.NewReader(r)
buf := bufio.NewWriter(w)
size := int64(0)
// Iterate through the files in the archive.
hdr, loopErr := tr.Next()
for loopErr == nil {
select {
case <-ctx.Done():
return 0, ctx.Err()
default:
}
// Note: path is used instead of filepath to prevent OS specific handling
// of the tar path
base := path.Base(hdr.Name)
if strings.HasPrefix(base, ociwclayer.WhiteoutPrefix) {
name := path.Join(path.Dir(hdr.Name), base[len(ociwclayer.WhiteoutPrefix):])
if rErr := w.Remove(filepath.FromSlash(name)); rErr != nil {
return 0, rErr
}
hdr, loopErr = tr.Next()
} else if hdr.Typeflag == tar.TypeLink {
if linkErr := w.AddLink(filepath.FromSlash(hdr.Name), filepath.FromSlash(hdr.Linkname)); linkErr != nil {
return 0, linkErr
}
hdr, loopErr = tr.Next()
} else {
name, fileSize, fileInfo, err := backuptar.FileInfoFromHeader(hdr)
if err != nil {
return 0, err
}
sddl, err := backuptar.SecurityDescriptorFromTarHeader(hdr)
if err != nil {
return 0, err
}
eadata, err := backuptar.ExtendedAttributesFromTarHeader(hdr)
if err != nil {
return 0, err
}
var reparse []byte
// As of now the only valid reparse data in a layer will be for a symlink. If file is
// a symlink set reparse attribute and ensure reparse data buffer isn't
// empty. Otherwise remove the reparse attributed.
fileInfo.FileAttributes &^= uint32(windows.FILE_ATTRIBUTE_REPARSE_POINT)
if hdr.Typeflag == tar.TypeSymlink {
reparse = backuptar.EncodeReparsePointFromTarHeader(hdr)
if len(reparse) > 0 {
fileInfo.FileAttributes |= uint32(windows.FILE_ATTRIBUTE_REPARSE_POINT)
}
}
if addErr := w.Add(filepath.FromSlash(name), fileInfo, fileSize, sddl, eadata, reparse); addErr != nil {
return 0, addErr
}
if hdr.Typeflag == tar.TypeReg {
if _, cpErr := io.Copy(buf, tr); cpErr != nil {
return 0, cpErr
}
}
size += fileSize
// Copy all the alternate data streams and return the next non-ADS header.
var ahdr *tar.Header
for {
ahdr, loopErr = tr.Next()
if loopErr != nil {
break
}
if ahdr.Typeflag != tar.TypeReg || !strings.HasPrefix(ahdr.Name, hdr.Name+":") {
hdr = ahdr
break
}
// stream names have following format: '<filename>:<stream name>:$DATA'
// $DATA is one of the valid types of streams. We currently only support
// data streams so fail if this is some other type of stream.
if !strings.HasSuffix(ahdr.Name, ":$DATA") {
return 0, fmt.Errorf("stream types other than $DATA are not supported, found: %s", ahdr.Name)
}
if addErr := w.AddAlternateStream(filepath.FromSlash(ahdr.Name), uint64(ahdr.Size)); addErr != nil {
return 0, addErr
}
if _, cpErr := io.Copy(buf, tr); cpErr != nil {
return 0, cpErr
}
}
}
if flushErr := buf.Flush(); flushErr != nil {
if loopErr == nil {
loopErr = flushErr
} else {
log.G(ctx).WithError(flushErr).Warn("flush buffer during layer write failed")
}
}
}
if !errors.Is(loopErr, io.EOF) {
return 0, loopErr
}
return size, nil
}
func DestroyCimLayer(layerPath string) error {
return cim.DestroyCimLayer(context.Background(), layerPath)
}

View File

@@ -5,6 +5,7 @@ package ociwclayer
import (
"archive/tar"
"context"
"errors"
"io"
"path/filepath"
@@ -62,7 +63,7 @@ func writeTarFromLayer(ctx context.Context, r wclayer.LayerReader, w io.Writer)
}
name, size, fileInfo, err := r.Next()
if err == io.EOF {
if errors.Is(err, io.EOF) {
break
}
if err != nil {

View File

@@ -6,6 +6,7 @@ import (
"archive/tar"
"bufio"
"context"
"errors"
"io"
"os"
"path"
@@ -102,7 +103,7 @@ func writeLayerFromTar(ctx context.Context, r io.Reader, w wclayer.LayerWriter,
totalSize += size
}
}
if err != io.EOF {
if !errors.Is(err, io.EOF) {
return 0, err
}
return totalSize, nil