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

@@ -20,6 +20,7 @@ linters:
# - typecheck
# - unused
- errorlint # error wrapping (eg, not using `errors.Is`, using `%s` instead of `%w` in `fmt.Errorf`)
- gofmt # whether code was gofmt-ed
- govet # enabled by default, but just to be sure
- nolintlint # ill-formed or insufficient nolint directives
@@ -53,6 +54,12 @@ issues:
text: "^ST1003: should not use underscores in package names$"
source: "^package cri_containerd$"
# don't bother with propper error wrapping in test code
- path: cri-containerd
linters:
- errorlint
text: "non-wrapping format verb for fmt.Errorf"
# This repo has a LOT of generated schema files, operating system bindings, and other
# things that ST1003 from stylecheck won't like (screaming case Windows api constants for example).
# There's also some structs that we *could* change the initialisms to be Go friendly

View File

@@ -9,15 +9,18 @@ It is primarily used in the [Moby](https://github.com/moby/moby) and [Containerd
## Building
While this repository can be used as a library of sorts to call the HCS apis, there are a couple binaries built out of the repository as well. The main ones being the Linux guest agent, and an implementation of the [runtime v2 containerd shim api](https://github.com/containerd/containerd/blob/master/runtime/v2/README.md).
### Linux Hyper-V Container Guest Agent
To build the Linux guest agent itself all that's needed is to set your GOOS to "Linux" and build out of ./cmd/gcs.
```powershell
C:\> $env:GOOS="linux"
C:\> go build .\cmd\gcs\
```
or on a Linux machine
```sh
> go build ./cmd/gcs
```
@@ -33,13 +36,15 @@ make all
```
If the build is successful, in the `./out` folder you should see:
```sh
> ls ./out/
delta.tar.gz initrd.img rootfs.tar.gz
```
### Containerd Shim
For info on the Runtime V2 API: https://github.com/containerd/containerd/blob/master/runtime/v2/README.md.
For info on the [Runtime V2 API](https://github.com/containerd/containerd/blob/master/runtime/v2/README.md).
Contrary to the typical Linux architecture of shim -> runc, the runhcs shim is used both to launch and manage the lifetime of containers.
@@ -48,7 +53,9 @@ C:\> $env:GOOS="windows"
C:\> go build .\cmd\containerd-shim-runhcs-v1
```
Then place the binary in the same directory that Containerd is located at in your environment. A default Containerd configuration file can be generated by running:
Then place the binary in the same directory that Containerd is located at in your environment.
A default Containerd configuration file can be generated by running:
```powershell
.\containerd.exe config default | Out-File "C:\Program Files\containerd\config.toml" -Encoding ascii
```
@@ -56,6 +63,7 @@ Then place the binary in the same directory that Containerd is located at in you
This config file will already have the shim set as the default runtime for cri interactions.
To trial using the shim out with ctr.exe:
```powershell
C:\> ctr.exe run --runtime io.containerd.runhcs.v1 --rm mcr.microsoft.com/windows/nanoserver:2004 windows-test cmd /c "echo Hello World!"
```
@@ -64,16 +72,69 @@ C:\> ctr.exe run --runtime io.containerd.runhcs.v1 --rm mcr.microsoft.com/window
This project welcomes contributions and suggestions. Most contributions require you to agree to a
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
the rights to use your contribution. For details, visit https://cla.microsoft.com.
the rights to use your contribution. For details, visit [Microsoft CLA](https://cla.microsoft.com).
When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
provided by the bot. You will only need to do this once across all repos using our CLA.
We also require that contributors [sign their commits](https://git-scm.com/docs/git-commit) using `git commit -s` or `git commit --signoff` to
certify they either authored the work themselves or otherwise have permission to use it in this project. Please see https://developercertificate.org/ for
more info, as well as to make sure that you can attest to the rules listed. Our CI uses the [DCO Github app](https://github.com/apps/dco) to ensure
that all commits in a given PR are signed-off.
We require that contributors sign their commits
to certify they either authored the work themselves or otherwise have permission to use it in this project.
We also require that contributors sign their commits using using [`git commit --signoff`][git-commit-s]
to certify they either authored the work themselves or otherwise have permission to use it in this project.
A range of commits can be signed off using [`git rebase --signoff`][git-rebase-s].
Please see [the developer certificate](https://developercertificate.org) for more info,
as well as to make sure that you can attest to the rules listed.
Our CI uses the [DCO Github app](https://github.com/apps/dco) to ensure that all commits in a given PR are signed-off.
### Linting
Code must pass a linting stage, which uses [`golangci-lint`][lint].
Since `./test` is a separate Go module, the linter is run from both the root and the
`test` directories. Additionally, the linter is run with `GOOS` set to both `windows` and
`linux`.
The linting settings are stored in [`.golangci.yaml`](./.golangci.yaml), and can be run
automatically with VSCode by adding the following to your workspace or folder settings:
```json
"go.lintTool": "golangci-lint",
"go.lintOnSave": "package",
```
Additional editor [integrations options are also available][lint-ide].
Alternatively, `golangci-lint` can be [installed][lint-install] and run locally:
```shell
# use . or specify a path to only lint a package
# to show all lint errors, use flags "--max-issues-per-linter=0 --max-same-issues=0"
> golangci-lint run
```
To run across the entire repo for both `GOOS=windows` and `linux`:
```powershell
> foreach ( $goos in ('windows', 'linux') ) {
foreach ( $repo in ('.', 'test') ) {
pwsh -Command "cd $repo && go env -w GOOS=$goos && golangci-lint.exe run --verbose"
}
}
```
### Go Generate
The pipeline checks that auto-generated code, via `go generate`, are up to date.
Similar to the [linting stage](#linting), `go generate` is run in both the root and test Go modules.
This can be done via:
```shell
> go generate ./...
> cd test && go generate ./...
```
## Code of Conduct
@@ -83,7 +144,7 @@ contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additio
## Dependencies
This project requires Golang 1.17 or newer to build.
This project requires Golang 1.18 or newer to build.
For system requirements to run this project, see the Microsoft docs on [Windows Container requirements](https://docs.microsoft.com/en-us/virtualization/windowscontainers/deploy-containers/system-requirements).
@@ -100,3 +161,10 @@ For additional details, see [Report a Computer Security Vulnerability](https://t
---------------
Copyright (c) 2018 Microsoft Corp. All rights reserved.
[lint]: https://golangci-lint.run/
[lint-ide]: https://golangci-lint.run/usage/integrations/#editor-integration
[lint-install]: https://golangci-lint.run/usage/install/#local-installation
[git-commit-s]: https://git-scm.com/docs/git-commit#Documentation/git-commit.txt--s
[git-rebase-s]: https://git-scm.com/docs/git-rebase#Documentation/git-rebase.txt---signoff

View File

@@ -38,3 +38,31 @@ func AttachLayerStorageFilter(ctx context.Context, layerPath string, layerData L
}
return nil
}
// AttachOverlayFilter sets up a filter of the given type on a writable container layer. Currently the only
// supported filter types are WCIFS & UnionFS (defined in internal/hcs/schema2/layer.go)
//
// `volumePath` is volume path at which writable layer is mounted. If the
// path does not end in a `\` the platform will append it automatically.
//
// `layerData` is the parent read-only layer data.
func AttachOverlayFilter(ctx context.Context, volumePath string, layerData LayerData) (err error) {
title := "hcsshim::AttachOverlayFilter"
ctx, span := oc.StartSpan(ctx, title) //nolint:ineffassign,staticcheck
defer span.End()
defer func() { oc.SetSpanStatus(span, err) }()
span.AddAttributes(
trace.StringAttribute("volumePath", volumePath),
)
bytes, err := json.Marshal(layerData)
if err != nil {
return err
}
err = hcsAttachOverlayFilter(volumePath, string(bytes))
if err != nil {
return errors.Wrap(err, "failed to attach overlay filter")
}
return nil
}

View File

@@ -4,7 +4,9 @@ package computestorage
import (
"context"
"encoding/json"
hcsschema "github.com/Microsoft/hcsshim/internal/hcs/schema2"
"github.com/Microsoft/hcsshim/internal/oc"
"github.com/pkg/errors"
"go.opencensus.io/trace"
@@ -26,3 +28,27 @@ func DetachLayerStorageFilter(ctx context.Context, layerPath string) (err error)
}
return nil
}
// DetachOverlayFilter detaches the filter on a writable container layer.
//
// `volumePath` is a path to writable container volume.
func DetachOverlayFilter(ctx context.Context, volumePath string, filterType hcsschema.FileSystemFilterType) (err error) {
title := "hcsshim::DetachOverlayFilter"
ctx, span := oc.StartSpan(ctx, title) //nolint:ineffassign,staticcheck
defer span.End()
defer func() { oc.SetSpanStatus(span, err) }()
span.AddAttributes(trace.StringAttribute("volumePath", volumePath))
layerData := LayerData{}
layerData.FilterType = filterType
bytes, err := json.Marshal(layerData)
if err != nil {
return err
}
err = hcsDetachOverlayFilter(volumePath, string(bytes))
if err != nil {
return errors.Wrap(err, "failed to detach overlay filter")
}
return nil
}

View File

@@ -19,14 +19,17 @@ import (
//sys hcsFormatWritableLayerVhd(handle windows.Handle) (hr error) = computestorage.HcsFormatWritableLayerVhd?
//sys hcsGetLayerVhdMountPath(vhdHandle windows.Handle, mountPath **uint16) (hr error) = computestorage.HcsGetLayerVhdMountPath?
//sys hcsSetupBaseOSVolume(layerPath string, volumePath string, options string) (hr error) = computestorage.HcsSetupBaseOSVolume?
//sys hcsAttachOverlayFilter(volumePath string, layerData string) (hr error) = computestorage.HcsAttachOverlayFilter?
//sys hcsDetachOverlayFilter(volumePath string, layerData string) (hr error) = computestorage.HcsDetachOverlayFilter?
type Version = hcsschema.Version
type Layer = hcsschema.Layer
// LayerData is the data used to describe parent layer information.
type LayerData struct {
SchemaVersion Version `json:"SchemaVersion,omitempty"`
Layers []Layer `json:"Layers,omitempty"`
SchemaVersion Version `json:"SchemaVersion,omitempty"`
Layers []Layer `json:"Layers,omitempty"`
FilterType hcsschema.FileSystemFilterType `json:"FilterType,omitempty"`
}
// ExportLayerOptions are the set of options that are used with the `computestorage.HcsExportLayer` syscall.

View File

@@ -43,8 +43,10 @@ var (
modcomputestorage = windows.NewLazySystemDLL("computestorage.dll")
procHcsAttachLayerStorageFilter = modcomputestorage.NewProc("HcsAttachLayerStorageFilter")
procHcsAttachOverlayFilter = modcomputestorage.NewProc("HcsAttachOverlayFilter")
procHcsDestroyLayer = modcomputestorage.NewProc("HcsDestroyLayer")
procHcsDetachLayerStorageFilter = modcomputestorage.NewProc("HcsDetachLayerStorageFilter")
procHcsDetachOverlayFilter = modcomputestorage.NewProc("HcsDetachOverlayFilter")
procHcsExportLayer = modcomputestorage.NewProc("HcsExportLayer")
procHcsFormatWritableLayerVhd = modcomputestorage.NewProc("HcsFormatWritableLayerVhd")
procHcsGetLayerVhdMountPath = modcomputestorage.NewProc("HcsGetLayerVhdMountPath")
@@ -83,6 +85,35 @@ func _hcsAttachLayerStorageFilter(layerPath *uint16, layerData *uint16) (hr erro
return
}
func hcsAttachOverlayFilter(volumePath string, layerData string) (hr error) {
var _p0 *uint16
_p0, hr = syscall.UTF16PtrFromString(volumePath)
if hr != nil {
return
}
var _p1 *uint16
_p1, hr = syscall.UTF16PtrFromString(layerData)
if hr != nil {
return
}
return _hcsAttachOverlayFilter(_p0, _p1)
}
func _hcsAttachOverlayFilter(volumePath *uint16, layerData *uint16) (hr error) {
hr = procHcsAttachOverlayFilter.Find()
if hr != nil {
return
}
r0, _, _ := syscall.Syscall(procHcsAttachOverlayFilter.Addr(), 2, uintptr(unsafe.Pointer(volumePath)), uintptr(unsafe.Pointer(layerData)), 0)
if int32(r0) < 0 {
if r0&0x1fff0000 == 0x00070000 {
r0 &= 0xffff
}
hr = syscall.Errno(r0)
}
return
}
func hcsDestroyLayer(layerPath string) (hr error) {
var _p0 *uint16
_p0, hr = syscall.UTF16PtrFromString(layerPath)
@@ -131,6 +162,35 @@ func _hcsDetachLayerStorageFilter(layerPath *uint16) (hr error) {
return
}
func hcsDetachOverlayFilter(volumePath string, layerData string) (hr error) {
var _p0 *uint16
_p0, hr = syscall.UTF16PtrFromString(volumePath)
if hr != nil {
return
}
var _p1 *uint16
_p1, hr = syscall.UTF16PtrFromString(layerData)
if hr != nil {
return
}
return _hcsDetachOverlayFilter(_p0, _p1)
}
func _hcsDetachOverlayFilter(volumePath *uint16, layerData *uint16) (hr error) {
hr = procHcsDetachOverlayFilter.Find()
if hr != nil {
return
}
r0, _, _ := syscall.Syscall(procHcsDetachOverlayFilter.Addr(), 2, uintptr(unsafe.Pointer(volumePath)), uintptr(unsafe.Pointer(layerData)), 0)
if int32(r0) < 0 {
if r0&0x1fff0000 == 0x00070000 {
r0 &= 0xffff
}
hr = syscall.Errno(r0)
}
return
}
func hcsExportLayer(layerPath string, exportFolderPath string, layerData string, options string) (hr error) {
var _p0 *uint16
_p0, hr = syscall.UTF16PtrFromString(layerPath)

View File

@@ -75,7 +75,7 @@ func init() {
func CreateContainer(id string, c *ContainerConfig) (Container, error) {
fullConfig, err := mergemaps.MergeJSON(c, createContainerAdditionalJSON)
if err != nil {
return nil, fmt.Errorf("failed to merge additional JSON '%s': %s", createContainerAdditionalJSON, err)
return nil, fmt.Errorf("failed to merge additional JSON '%s': %w", createContainerAdditionalJSON, err)
}
system, err := hcs.CreateComputeSystem(context.Background(), id, fullConfig)

View File

@@ -115,6 +115,7 @@ func (e *ContainerError) Error() string {
s += " encountered an error during " + e.Operation
}
//nolint:errorlint // legacy code
switch e.Err.(type) {
case nil:
break
@@ -145,6 +146,7 @@ func (e *ProcessError) Error() string {
s += " encountered an error during " + e.Operation
}
//nolint:errorlint // legacy code
switch e.Err.(type) {
case nil:
break
@@ -166,10 +168,10 @@ func (e *ProcessError) Error() string {
// already exited, or does not exist. Both IsAlreadyStopped and IsNotExist
// will currently return true when the error is ErrElementNotFound.
func IsNotExist(err error) bool {
if _, ok := err.(EndpointNotFoundError); ok {
if _, ok := err.(EndpointNotFoundError); ok { //nolint:errorlint // legacy code
return true
}
if _, ok := err.(NetworkNotFoundError); ok {
if _, ok := err.(NetworkNotFoundError); ok { //nolint:errorlint // legacy code
return true
}
return hcs.IsNotExist(getInnerError(err))
@@ -224,6 +226,7 @@ func IsAccessIsDenied(err error) bool {
}
func getInnerError(err error) error {
//nolint:errorlint // legacy code
switch pe := err.(type) {
case nil:
return nil
@@ -236,14 +239,14 @@ func getInnerError(err error) error {
}
func convertSystemError(err error, c *container) error {
if serr, ok := err.(*hcs.SystemError); ok {
if serr, ok := err.(*hcs.SystemError); ok { //nolint:errorlint // legacy code
return &ContainerError{Container: c, Operation: serr.Op, Err: serr.Err, Events: serr.Events}
}
return err
}
func convertProcessError(err error, p *process) error {
if perr, ok := err.(*hcs.ProcessError); ok {
if perr, ok := err.(*hcs.ProcessError); ok { //nolint:errorlint // legacy code
return &ProcessError{Process: p, Operation: perr.Op, Err: perr.Err, Events: perr.Events}
}
return err

View File

@@ -185,6 +185,8 @@ func ReadDMVerityInfoReader(r io.Reader) (*VerityInfo, error) {
block := make([]byte, blockSize)
if s, err := r.Read(block); err != nil || s != blockSize {
if err != nil {
// TODO (go1.20): use multierror via fmt.Errorf("...: %w; ...: %w", ...)
//nolint:errorlint // non-wrapping format verb for fmt.Errorf
return nil, fmt.Errorf("%s: %w", ErrSuperBlockReadFailure, err)
}
return nil, fmt.Errorf("unexpected bytes read expected=%d actual=%d: %w", blockSize, s, ErrSuperBlockReadFailure)
@@ -193,6 +195,8 @@ func ReadDMVerityInfoReader(r io.Reader) (*VerityInfo, error) {
dmvSB := &dmveritySuperblock{}
b := bytes.NewBuffer(block)
if err := binary.Read(b, binary.LittleEndian, dmvSB); err != nil {
// TODO (go1.20): use multierror via fmt.Errorf("...: %w; ...: %w", ...)
//nolint:errorlint // non-wrapping format verb for fmt.Errorf
return nil, fmt.Errorf("%s: %w", ErrSuperBlockParseFailure, err)
}
@@ -202,6 +206,8 @@ func ReadDMVerityInfoReader(r io.Reader) (*VerityInfo, error) {
if s, err := r.Read(block); err != nil || s != blockSize {
if err != nil {
// TODO (go1.20): use multierror via fmt.Errorf("...: %w; ...: %w", ...)
//nolint:errorlint // non-wrapping format verb for fmt.Errorf
return nil, fmt.Errorf("%s: %w", ErrRootHashReadFailure, err)
}
return nil, fmt.Errorf("unexpected bytes read expected=%d, actual=%d: %w", blockSize, s, ErrRootHashReadFailure)

View File

@@ -604,7 +604,7 @@ func (w *Writer) Create(name string, f *File) error {
}
child, err := w.makeInode(f, reuse)
if err != nil {
return fmt.Errorf("%s: %s", name, err)
return fmt.Errorf("%s: %w", name, err)
}
if existing != child {
if existing != nil {

View File

@@ -85,7 +85,7 @@ func ConvertTarToExt4(r io.Reader, w io.ReadWriteSeeker, options ...Option) erro
fs := compactext4.NewWriter(w, p.ext4opts...)
for {
hdr, err := t.Next()
if err == io.EOF {
if errors.Is(err, io.EOF) {
break
}
if err != nil {
@@ -301,7 +301,7 @@ func Ext4FileSystemSize(r io.ReadSeeker) (int64, int, error) {
func ConvertAndComputeRootDigest(r io.Reader) (string, error) {
out, err := os.CreateTemp("", "")
if err != nil {
return "", fmt.Errorf("failed to create temporary file: %s", err)
return "", fmt.Errorf("failed to create temporary file: %w", err)
}
defer func() {
_ = os.Remove(out.Name())
@@ -313,16 +313,16 @@ func ConvertAndComputeRootDigest(r io.Reader) (string, error) {
MaximumDiskSize(dmverity.RecommendedVHDSizeGB),
}
if err := ConvertTarToExt4(r, out, options...); err != nil {
return "", fmt.Errorf("failed to convert tar to ext4: %s", err)
return "", fmt.Errorf("failed to convert tar to ext4: %w", err)
}
if _, err := out.Seek(0, io.SeekStart); err != nil {
return "", fmt.Errorf("failed to seek start on temp file when creating merkle tree: %s", err)
return "", fmt.Errorf("failed to seek start on temp file when creating merkle tree: %w", err)
}
tree, err := dmverity.MerkleTree(bufio.NewReaderSize(out, dmverity.MerkleTreeBufioSize))
if err != nil {
return "", fmt.Errorf("failed to create merkle tree: %s", err)
return "", fmt.Errorf("failed to create merkle tree: %w", err)
}
hash := dmverity.RootHash(tree)

View File

@@ -6,11 +6,12 @@ import (
"errors"
"fmt"
"github.com/sirupsen/logrus"
"golang.org/x/sys/windows"
"github.com/Microsoft/hcsshim/internal/hcs"
"github.com/Microsoft/hcsshim/internal/hcserror"
"github.com/Microsoft/hcsshim/internal/interop"
"github.com/sirupsen/logrus"
"golang.org/x/sys/windows"
)
var (
@@ -63,8 +64,8 @@ func (e *HcnError) Error() string {
}
func CheckErrorWithCode(err error, code ErrorCode) bool {
hcnError, ok := err.(*HcnError)
if ok {
var hcnError *HcnError
if errors.As(err, &hcnError) {
return hcnError.code == code
}
return false
@@ -81,7 +82,7 @@ func IsPortAlreadyExistsError(err error) bool {
func new(hr error, title string, rest string) error {
err := &HcnError{}
hcsError := hcserror.New(hr, title, rest)
err.HcsError = hcsError.(*hcserror.HcsError)
err.HcsError = hcsError.(*hcserror.HcsError) //nolint:errorlint
err.code = ErrorCode(hcserror.Win32FromError(hr))
return err
}
@@ -97,6 +98,8 @@ type NetworkNotFoundError struct {
NetworkID string
}
var _ error = NetworkNotFoundError{}
func (e NetworkNotFoundError) Error() string {
if e.NetworkName != "" {
return fmt.Sprintf("Network name %q not found", e.NetworkName)
@@ -110,6 +113,8 @@ type EndpointNotFoundError struct {
EndpointID string
}
var _ error = EndpointNotFoundError{}
func (e EndpointNotFoundError) Error() string {
if e.EndpointName != "" {
return fmt.Sprintf("Endpoint name %q not found", e.EndpointName)
@@ -122,6 +127,8 @@ type NamespaceNotFoundError struct {
NamespaceID string
}
var _ error = NamespaceNotFoundError{}
func (e NamespaceNotFoundError) Error() string {
return fmt.Sprintf("Namespace ID %q not found", e.NamespaceID)
}
@@ -131,6 +138,8 @@ type LoadBalancerNotFoundError struct {
LoadBalancerId string
}
var _ error = LoadBalancerNotFoundError{}
func (e LoadBalancerNotFoundError) Error() string {
return fmt.Sprintf("LoadBalancer %q not found", e.LoadBalancerId)
}
@@ -140,6 +149,8 @@ type RouteNotFoundError struct {
RouteId string
}
var _ error = RouteNotFoundError{}
func (e RouteNotFoundError) Error() string {
return fmt.Sprintf("SDN Route %q not found", e.RouteId)
}
@@ -147,19 +158,31 @@ func (e RouteNotFoundError) Error() string {
// IsNotFoundError returns a boolean indicating whether the error was caused by
// a resource not being found.
func IsNotFoundError(err error) bool {
switch pe := err.(type) {
case NetworkNotFoundError:
// Calling [errors.As] in a loop over `[]error{NetworkNotFoundError{}, ...}` will not work,
// since the loop variable will be an interface type (ie, `error`) and `errors.As(error, *error)` will
// always succeed.
// Unless golang adds loops over (or arrays of) types, we need to manually call `errors.As` for
// each potential error type.
//
// Also, for T = NetworkNotFoundError and co, the error implementation is for T, not *T
if e := (NetworkNotFoundError{}); errors.As(err, &e) {
return true
case EndpointNotFoundError:
return true
case NamespaceNotFoundError:
return true
case LoadBalancerNotFoundError:
return true
case RouteNotFoundError:
return true
case *hcserror.HcsError:
return pe.Err == hcs.ErrElementNotFound
}
if e := (EndpointNotFoundError{}); errors.As(err, &e) {
return true
}
if e := (NamespaceNotFoundError{}); errors.As(err, &e) {
return true
}
if e := (LoadBalancerNotFoundError{}); errors.As(err, &e) {
return true
}
if e := (RouteNotFoundError{}); errors.As(err, &e) {
return true
}
if e := (&hcserror.HcsError{}); errors.As(err, &e) {
return errors.Is(e.Err, hcs.ErrElementNotFound)
}
return false
}

View File

@@ -4,6 +4,7 @@ package hcn
import (
"encoding/json"
"errors"
"os"
"syscall"
@@ -378,7 +379,8 @@ func (namespace *HostComputeNamespace) Sync() error {
shimPath := runhcs.VMPipePath(cfg.HostUniqueID)
if err := runhcs.IssueVMRequest(shimPath, &req); err != nil {
// The shim is likely gone. Simply ignore the sync as if it didn't exist.
if perr, ok := err.(*os.PathError); ok && perr.Err == syscall.ERROR_FILE_NOT_FOUND {
var perr *os.PathError
if errors.As(err, &perr) && errors.Is(perr.Err, syscall.ERROR_FILE_NOT_FOUND) {
// Remove the reg key there is no point to try again
_ = cfg.Remove()
return nil

View File

@@ -63,7 +63,7 @@ func (process *Process) SystemID() string {
}
func (process *Process) processSignalResult(ctx context.Context, err error) (bool, error) {
switch err {
switch err { //nolint:errorlint
case nil:
return true, nil
case ErrVmcomputeOperationInvalidState, ErrComputeSystemDoesNotExist, ErrElementNotFound:

View File

@@ -9,6 +9,13 @@
package hcsschema
type FileSystemFilterType string
const (
UnionFS FileSystemFilterType = "UnionFS"
WCIFS FileSystemFilterType = "WCIFS"
)
type Layer struct {
Id string `json:"Id,omitempty"`

View File

@@ -0,0 +1,13 @@
package hcsschema
// NOTE: manually added
type RegistryHive string
// List of RegistryHive
const (
RegistryHive_SYSTEM RegistryHive = "System"
RegistryHive_SOFTWARE RegistryHive = "Software"
RegistryHive_SECURITY RegistryHive = "Security"
RegistryHive_SAM RegistryHive = "Sam"
)

View File

@@ -10,7 +10,7 @@
package hcsschema
type RegistryKey struct {
Hive string `json:"Hive,omitempty"`
Hive RegistryHive `json:"Hive,omitempty"`
Name string `json:"Name,omitempty"`

View File

@@ -14,7 +14,7 @@ type RegistryValue struct {
Name string `json:"Name,omitempty"`
Type_ string `json:"Type,omitempty"`
Type_ RegistryValueType `json:"Type,omitempty"`
// One and only one value type must be set.
StringValue string `json:"StringValue,omitempty"`

View File

@@ -0,0 +1,17 @@
package hcsschema
// NOTE: manually added
type RegistryValueType string
// List of RegistryValueType
const (
RegistryValueType_NONE RegistryValueType = "None"
RegistryValueType_STRING RegistryValueType = "String"
RegistryValueType_EXPANDED_STRING RegistryValueType = "ExpandedString"
RegistryValueType_MULTI_STRING RegistryValueType = "MultiString"
RegistryValueType_BINARY RegistryValueType = "Binary"
RegistryValueType_D_WORD RegistryValueType = "DWord"
RegistryValueType_Q_WORD RegistryValueType = "QWord"
RegistryValueType_CUSTOM_TYPE RegistryValueType = "CustomType"
)

View File

@@ -97,7 +97,7 @@ func CreateComputeSystem(ctx context.Context, id string, hcsDocumentInterface in
events, err := processAsyncHcsResult(ctx, createError, resultJSON, computeSystem.callbackNumber,
hcsNotificationSystemCreateCompleted, &timeout.SystemCreate)
if err != nil {
if err == ErrTimeout {
if errors.Is(err, ErrTimeout) {
// Terminate the compute system if it still exists. We're okay to
// ignore a failure here.
_ = computeSystem.Terminate(ctx)
@@ -238,7 +238,7 @@ func (computeSystem *System) Shutdown(ctx context.Context) error {
resultJSON, err := vmcompute.HcsShutdownComputeSystem(ctx, computeSystem.handle, "")
events := processHcsResult(ctx, resultJSON)
switch err {
switch err { //nolint:errorlint
case nil, ErrVmcomputeAlreadyStopped, ErrComputeSystemDoesNotExist, ErrVmcomputeOperationPending:
default:
return makeSystemError(computeSystem, operation, err, events)
@@ -259,7 +259,7 @@ func (computeSystem *System) Terminate(ctx context.Context) error {
resultJSON, err := vmcompute.HcsTerminateComputeSystem(ctx, computeSystem.handle, "")
events := processHcsResult(ctx, resultJSON)
switch err {
switch err { //nolint:errorlint
case nil, ErrVmcomputeAlreadyStopped, ErrComputeSystemDoesNotExist, ErrVmcomputeOperationPending:
default:
return makeSystemError(computeSystem, operation, err, events)
@@ -279,7 +279,7 @@ func (computeSystem *System) waitBackground() {
span.AddAttributes(trace.StringAttribute("cid", computeSystem.id))
err := waitForNotification(ctx, computeSystem.callbackNumber, hcsNotificationSystemExited, nil)
switch err {
switch err { //nolint:errorlint
case nil:
log.G(ctx).Debug("system exited")
case ErrVmcomputeUnexpectedExit:

View File

@@ -31,7 +31,7 @@ func hnsCallRawResponse(method, path, request string) (*hnsResponse, error) {
func hnsCall(method, path, request string, returnResponse interface{}) error {
hnsresponse, err := hnsCallRawResponse(method, path, request)
if err != nil {
return fmt.Errorf("failed during hnsCallRawResponse: %v", err)
return fmt.Errorf("failed during hnsCallRawResponse: %w", err)
}
if !hnsresponse.Success {
return fmt.Errorf("hns failed with error : %s", hnsresponse.Error)

View File

@@ -56,7 +56,7 @@ func issueNamespaceRequest(id *string, method, subpath string, request interface
if strings.Contains(err.Error(), "Element not found.") {
return nil, os.ErrNotExist
}
return nil, fmt.Errorf("%s %s: %s", method, hnspath, err)
return nil, fmt.Errorf("%s %s: %w", method, hnspath, err)
}
return &ns, err
}
@@ -86,7 +86,7 @@ func GetNamespaceEndpoints(id string) ([]string, error) {
var endpoint namespaceEndpointRequest
err = json.Unmarshal(rsrc.Data, &endpoint)
if err != nil {
return nil, fmt.Errorf("unmarshal endpoint: %s", err)
return nil, fmt.Errorf("unmarshal endpoint: %w", err)
}
endpoints = append(endpoints, endpoint.ID)
}

View File

@@ -4,6 +4,7 @@ package jobobject
import (
"context"
"errors"
"fmt"
"sync"
"unsafe"
@@ -59,7 +60,7 @@ func pollIOCP(ctx context.Context, iocpHandle windows.Handle) {
}).Warn("failed to parse job object message")
continue
}
if err := msq.Enqueue(notification); err == queue.ErrQueueClosed {
if err := msq.Enqueue(notification); errors.Is(err, queue.ErrQueueClosed) {
// Write will only return an error when the queue is closed.
// The only time a queue would ever be closed is when we call `Close` on
// the job it belongs to which also removes it from the jobMap, so something

View File

@@ -374,7 +374,7 @@ func (job *JobObject) Pids() ([]uint32, error) {
return []uint32{}, nil
}
if err != winapi.ERROR_MORE_DATA {
if err != winapi.ERROR_MORE_DATA { //nolint:errorlint
return nil, fmt.Errorf("failed initial query for PIDs in job object: %w", err)
}

View File

@@ -143,6 +143,13 @@ func (job *JobObject) SetCPUAffinity(affinityBitMask uint64) error {
return err
}
info.BasicLimitInformation.LimitFlags |= uint32(windows.JOB_OBJECT_LIMIT_AFFINITY)
// We really, really shouldn't be running on 32 bit, but just in case (and to satisfy CodeQL) ...
const maxUintptr = ^uintptr(0)
if affinityBitMask > uint64(maxUintptr) {
return fmt.Errorf("affinity bitmask (%d) exceeds max allowable value (%d)", affinityBitMask, maxUintptr)
}
info.BasicLimitInformation.Affinity = uintptr(affinityBitMask)
return job.setExtendedInformation(info)
}

View File

@@ -104,6 +104,7 @@ func encode(v interface{}) (_ []byte, err error) {
if jErr := enc.Encode(v); jErr != nil {
if err != nil {
// TODO (go1.20): use multierror via fmt.Errorf("...: %w; ...: %w", ...)
//nolint:errorlint // non-wrapping format verb for fmt.Errorf
return nil, fmt.Errorf("protojson encoding: %v; json encoding: %w", err, jErr)
}
return nil, fmt.Errorf("json encoding: %w", jErr)

View File

@@ -46,6 +46,7 @@ const (
ExpectedType = "expected-type"
Bool = "bool"
Int32 = "int32"
Uint32 = "uint32"
Uint64 = "uint64"

View File

@@ -126,7 +126,7 @@ func (pa *PoolAllocator) Allocate(size uint64) (MappedRegion, error) {
// this means that there are no more regions for the current class, try expanding
if nextCls != memCls {
if err := pa.split(memCls); err != nil {
if err == ErrInvalidMemoryClass {
if errors.Is(err, ErrInvalidMemoryClass) {
return nil, ErrNotEnoughSpace
}
return nil, err
@@ -147,7 +147,7 @@ func (pa *PoolAllocator) Allocate(size uint64) (MappedRegion, error) {
}
// Release marks a memory region of class `memCls` and offset `offset` as free and tries to merge smaller regions into
// a bigger one
// a bigger one.
func (pa *PoolAllocator) Release(reg MappedRegion) error {
mp := pa.pools[reg.Type()]
if mp == nil {
@@ -164,7 +164,7 @@ func (pa *PoolAllocator) Release(reg MappedRegion) error {
return ErrNotAllocated
}
if err := pa.merge(n.parent); err != nil {
if err != ErrEarlyMerge {
if !errors.Is(err, ErrEarlyMerge) {
return err
}
}

View File

@@ -4,6 +4,7 @@ package regstate
import (
"encoding/json"
"errors"
"fmt"
"net/url"
"os"
@@ -44,8 +45,8 @@ func (err *NotFoundError) Error() string {
}
func IsNotFoundError(err error) bool {
_, ok := err.(*NotFoundError)
return ok
var e *NotFoundError
return errors.As(err, &e)
}
type NoStateError struct {
@@ -152,7 +153,8 @@ func (k *Key) openid(id string) (*Key, error) {
escaped := url.PathEscape(id)
fullpath := filepath.Join(k.Name, escaped)
nk, err := k.open(escaped)
if perr, ok := err.(*os.PathError); ok && perr.Err == syscall.ERROR_FILE_NOT_FOUND {
var perr *os.PathError
if errors.As(err, &perr) && errors.Is(perr.Err, syscall.ERROR_FILE_NOT_FOUND) {
return nil, &NotFoundError{id}
}
if err != nil {
@@ -165,7 +167,7 @@ func (k *Key) Remove(id string) error {
escaped := url.PathEscape(id)
err := registry.DeleteKey(k.Key, escaped)
if err != nil {
if err == syscall.ERROR_FILE_NOT_FOUND {
if err == syscall.ERROR_FILE_NOT_FOUND { //nolint:errorlint
return &NotFoundError{id}
}
return &os.PathError{Op: "RegDeleteKey", Path: filepath.Join(k.Name, escaped), Err: err}
@@ -215,7 +217,7 @@ func (k *Key) set(id string, create bool, key string, state interface{}) error {
err = sk.SetBinaryValue(key, js)
}
if err != nil {
if err == syscall.ERROR_FILE_NOT_FOUND {
if err == syscall.ERROR_FILE_NOT_FOUND { //nolint:errorlint
return &NoStateError{id, key}
}
return &os.PathError{Op: "RegSetValueEx", Path: sk.Name + ":" + key, Err: err}
@@ -239,7 +241,7 @@ func (k *Key) Clear(id, key string) error {
defer sk.Close()
err = sk.DeleteValue(key)
if err != nil {
if err == syscall.ERROR_FILE_NOT_FOUND {
if err == syscall.ERROR_FILE_NOT_FOUND { //nolint:errorlint
return &NoStateError{id, key}
}
return &os.PathError{Op: "RegDeleteValue", Path: sk.Name + ":" + key, Err: err}
@@ -278,7 +280,7 @@ func (k *Key) Get(id, key string, state interface{}) error {
js, _, err = sk.GetBinaryValue(key)
}
if err != nil {
if err == syscall.ERROR_FILE_NOT_FOUND {
if err == syscall.ERROR_FILE_NOT_FOUND { //nolint:errorlint
return &NoStateError{id, key}
}
return &os.PathError{Op: "RegQueryValueEx", Path: sk.Name + ":" + key, Err: err}

View File

@@ -243,7 +243,7 @@ func RemoveRelative(path string, root *os.File) error {
if err == nil {
defer f.Close()
err = deleteOnClose(f)
if err == syscall.ERROR_ACCESS_DENIED {
if err == syscall.ERROR_ACCESS_DENIED { //nolint:errorlint
// Maybe the file is marked readonly. Clear the bit and retry.
_ = clearReadOnly(f)
err = deleteOnClose(f)

View File

@@ -0,0 +1,3 @@
// vhdx package adds the utility methods necessary to deal with the vhdx that are used as the scratch
// space for the containers and the uvm.
package vhdx

View File

@@ -0,0 +1,233 @@
//go:build windows
package vhdx
import (
"bytes"
"context"
"encoding/binary"
"fmt"
"os"
"syscall"
"unsafe"
"github.com/Microsoft/go-winio/pkg/guid"
"github.com/Microsoft/go-winio/vhd"
"github.com/Microsoft/hcsshim/internal/log"
"github.com/Microsoft/hcsshim/internal/oc"
"github.com/sirupsen/logrus"
"go.opencensus.io/trace"
"golang.org/x/sys/windows"
)
const _IOCTL_DISK_GET_DRIVE_LAYOUT_EX = 0x00070050
var partitionBasicDataGUID = guid.GUID{
Data1: 0xebd0a0a2,
Data2: 0xb9e5,
Data3: 0x4433,
Data4: [8]byte{0x87, 0xc0, 0x68, 0xb6, 0xb7, 0x26, 0x99, 0xc7},
}
const (
partitionStyleMBR uint32 = iota
partitionStyleGPT
partitionStyleRaw
)
// type partitionInformationMBR struct {
// PartitionType uint8
// BootIndicator uint8
// RecognizedPartition uint8
// HiddenSectors uint32
// PartitionId guid.GUID
// }
type partitionInformationGPT struct {
PartitionType guid.GUID
PartitionId guid.GUID
Attributes uint64
Name [72]byte // wide char
}
type partitionInformationEx struct {
PartitionStyle uint32
StartingOffset int64
PartitionLength int64
PartitionNumber uint32
RewritePartition uint8
IsServicePartition uint8
_ uint16
// A union of partitionInformationMBR and partitionInformationGPT
// since partitionInformationGPT is largest with 112 bytes
GptMbrUnion [112]byte
}
type driveLayoutInformationGPT struct {
DiskID guid.GUID
StartingUsableOffset int64
UsableLength int64
MaxPartitionCount uint32
}
// type driveLayoutInformationMBR struct {
// Signature uint32
// Checksum uint32
// }
type driveLayoutInformationEx struct {
PartitionStyle uint32
PartitionCount uint32
// A union of driveLayoutInformationGPT and driveLayoutInformationMBR
// since driveLayoutInformationGPT is largest with 40 bytes
GptMbrUnion [40]byte
PartitionEntry [1]partitionInformationEx
}
// Takes the physical path of a disk and retrieves the drive layout information of that disk. Returns the
// driveLayoutInformationEx struct and a slice of partitionInfomrationEx struct containing one element for
// each partition found on the vhdx. Note: some of the members like (GptMbrUnion) of these structs are raw
// byte arrays and it is the responsibility of the calling function to properly parse them.
func getDriveLayout(ctx context.Context, drivePhysicalPath string) (driveLayoutInformationEx, []partitionInformationEx, error) {
var (
outBytes uint32
err error
volume *os.File
)
layoutData := struct {
info driveLayoutInformationEx
// driveLayoutInformationEx has a flexible array member at the end. The data returned
// by IOCTL_DISK_GET_DRIVE_LAYOUT_EX usually has driveLayoutInformationEx.PartitionCount
// number of elements in this array. For all practical purposes we don't expect to have
// more than 64 partitions in a container/uvm vhdx.
partitions [63]partitionInformationEx
}{}
volume, err = os.OpenFile(drivePhysicalPath, os.O_RDONLY, 0)
if err != nil {
return layoutData.info, layoutData.partitions[:0], fmt.Errorf("failed to open drive: %w", err)
}
defer volume.Close()
err = windows.DeviceIoControl(windows.Handle(volume.Fd()),
_IOCTL_DISK_GET_DRIVE_LAYOUT_EX,
nil,
0,
(*byte)(unsafe.Pointer(&layoutData)),
uint32(unsafe.Sizeof(layoutData)),
&outBytes,
nil)
if err != nil {
return layoutData.info, layoutData.partitions[:0], fmt.Errorf("IOCTL to get disk layout failed: %w", err)
}
if layoutData.info.PartitionCount == 0 {
return layoutData.info, []partitionInformationEx{}, nil
} else {
// parse the retrieved data into driveLayoutInformationEx and partitionInformationEx
partitions := make([]partitionInformationEx, layoutData.info.PartitionCount)
partitions[0] = layoutData.info.PartitionEntry[0]
copy(partitions[1:], layoutData.partitions[:layoutData.info.PartitionCount-1])
return layoutData.info, partitions, nil
}
}
// Scratch VHDs are formatted with GPT style and have 1 MSFT_RESERVED
// partition and 1 BASIC_DATA partition. This struct contains the
// partitionID of this BASIC_DATA partition and the DiskID of this
// scratch vhdx.
type ScratchVhdxPartitionInfo struct {
DiskID guid.GUID
PartitionID guid.GUID
}
// Returns the VhdxInfo of a GPT vhdx at path vhdxPath.
func GetScratchVhdPartitionInfo(ctx context.Context, vhdxPath string) (_ ScratchVhdxPartitionInfo, err error) {
var (
diskHandle syscall.Handle
driveLayout driveLayoutInformationEx
partitions []partitionInformationEx
gptDriveLayout driveLayoutInformationGPT
gptPartitionInfo partitionInformationGPT
volumePath string
)
title := "hcsshim::GetScratchVhdPartitionInfo"
ctx, span := trace.StartSpan(ctx, title)
defer span.End()
defer func() { oc.SetSpanStatus(span, err) }()
span.AddAttributes(
trace.StringAttribute("path", vhdxPath))
diskHandle, err = vhd.OpenVirtualDisk(vhdxPath, vhd.VirtualDiskAccessNone, vhd.OpenVirtualDiskFlagNone)
if err != nil {
return ScratchVhdxPartitionInfo{}, fmt.Errorf("get scratch vhd info failed: %w", err)
}
defer func() {
if closeErr := syscall.CloseHandle(diskHandle); closeErr != nil {
log.G(ctx).WithFields(logrus.Fields{
"disk path": vhdxPath,
"error": closeErr,
}).Warn("failed to close vhd handle")
}
}()
err = vhd.AttachVirtualDisk(diskHandle, vhd.AttachVirtualDiskFlagNone, &vhd.AttachVirtualDiskParameters{Version: 2})
if err != nil {
return ScratchVhdxPartitionInfo{}, fmt.Errorf("get scratch vhd info failed: %w", err)
}
defer func() {
if detachErr := vhd.DetachVirtualDisk(diskHandle); detachErr != nil {
log.G(ctx).WithFields(logrus.Fields{
"disk path": vhdxPath,
"error": detachErr,
}).Warn("failed to detach vhd")
}
}()
volumePath, err = vhd.GetVirtualDiskPhysicalPath(diskHandle)
if err != nil {
return ScratchVhdxPartitionInfo{}, fmt.Errorf("get vhd physical path: %w", err)
}
driveLayout, partitions, err = getDriveLayout(ctx, volumePath)
if err != nil {
return ScratchVhdxPartitionInfo{}, err
}
if driveLayout.PartitionStyle != partitionStyleGPT {
return ScratchVhdxPartitionInfo{}, fmt.Errorf("drive Layout:Expected partition style GPT(%d) found %d", partitionStyleGPT, driveLayout.PartitionStyle)
}
if driveLayout.PartitionCount != 2 || len(partitions) != 2 {
return ScratchVhdxPartitionInfo{}, fmt.Errorf("expected exactly 2 partitions. Got %d partitions and partition count of %d", len(partitions), driveLayout.PartitionCount)
}
if partitions[1].PartitionStyle != partitionStyleGPT {
return ScratchVhdxPartitionInfo{}, fmt.Errorf("partition Info:Expected partition style GPT(%d) found %d", partitionStyleGPT, partitions[1].PartitionStyle)
}
bufReader := bytes.NewBuffer(driveLayout.GptMbrUnion[:])
if err := binary.Read(bufReader, binary.LittleEndian, &gptDriveLayout); err != nil {
return ScratchVhdxPartitionInfo{}, fmt.Errorf("failed to parse drive GPT layout: %w", err)
}
bufReader = bytes.NewBuffer(partitions[1].GptMbrUnion[:])
if err := binary.Read(bufReader, binary.LittleEndian, &gptPartitionInfo); err != nil {
return ScratchVhdxPartitionInfo{}, fmt.Errorf("failed to parse GPT partition info: %w", err)
}
if gptPartitionInfo.PartitionType != partitionBasicDataGUID {
return ScratchVhdxPartitionInfo{}, fmt.Errorf("expected partition type to have %s GUID found %s instead", partitionBasicDataGUID, gptPartitionInfo.PartitionType)
}
log.G(ctx).WithFields(logrus.Fields{
"Disk ID": gptDriveLayout.DiskID,
"GPT Partition ID": gptPartitionInfo.PartitionId,
}).Debug("Scratch VHD partition info")
return ScratchVhdxPartitionInfo{DiskID: gptDriveLayout.DiskID, PartitionID: gptPartitionInfo.PartitionId}, nil
}

View File

@@ -104,7 +104,7 @@ func execute(ctx gcontext.Context, timeout time.Duration, f func() error) error
}()
select {
case <-ctx.Done():
if ctx.Err() == gcontext.DeadlineExceeded {
if ctx.Err() == gcontext.DeadlineExceeded { //nolint:errorlint
log.G(ctx).WithField(logfields.Timeout, trueTimeout).
Warning("Syscall did not complete within operation timeout. This may indicate a platform issue. " +
"If it appears to be making no forward progress, obtain the stacks and see if there is a syscall " +
@@ -150,7 +150,7 @@ func HcsCreateComputeSystem(ctx gcontext.Context, id string, configuration strin
if result != "" {
span.AddAttributes(trace.StringAttribute("result", result))
}
if hr != errVmcomputeOperationPending {
if hr != errVmcomputeOperationPending { //nolint:errorlint // explicitly returned
oc.SetSpanStatus(span, hr)
}
}()
@@ -205,7 +205,7 @@ func HcsStartComputeSystem(ctx gcontext.Context, computeSystem HcsSystem, option
if result != "" {
span.AddAttributes(trace.StringAttribute("result", result))
}
if hr != errVmcomputeOperationPending {
if hr != errVmcomputeOperationPending { //nolint:errorlint // explicitly returned
oc.SetSpanStatus(span, hr)
}
}()
@@ -228,7 +228,7 @@ func HcsShutdownComputeSystem(ctx gcontext.Context, computeSystem HcsSystem, opt
if result != "" {
span.AddAttributes(trace.StringAttribute("result", result))
}
if hr != errVmcomputeOperationPending {
if hr != errVmcomputeOperationPending { //nolint:errorlint // explicitly returned
oc.SetSpanStatus(span, hr)
}
}()
@@ -251,7 +251,7 @@ func HcsTerminateComputeSystem(ctx gcontext.Context, computeSystem HcsSystem, op
if result != "" {
span.AddAttributes(trace.StringAttribute("result", result))
}
if hr != errVmcomputeOperationPending {
if hr != errVmcomputeOperationPending { //nolint:errorlint // explicitly returned
oc.SetSpanStatus(span, hr)
}
}()
@@ -274,7 +274,7 @@ func HcsPauseComputeSystem(ctx gcontext.Context, computeSystem HcsSystem, option
if result != "" {
span.AddAttributes(trace.StringAttribute("result", result))
}
if hr != errVmcomputeOperationPending {
if hr != errVmcomputeOperationPending { //nolint:errorlint // explicitly returned
oc.SetSpanStatus(span, hr)
}
}()
@@ -297,7 +297,7 @@ func HcsResumeComputeSystem(ctx gcontext.Context, computeSystem HcsSystem, optio
if result != "" {
span.AddAttributes(trace.StringAttribute("result", result))
}
if hr != errVmcomputeOperationPending {
if hr != errVmcomputeOperationPending { //nolint:errorlint // explicitly returned
oc.SetSpanStatus(span, hr)
}
}()
@@ -621,7 +621,7 @@ func HcsSaveComputeSystem(ctx gcontext.Context, computeSystem HcsSystem, options
if result != "" {
span.AddAttributes(trace.StringAttribute("result", result))
}
if hr != errVmcomputeOperationPending {
if hr != errVmcomputeOperationPending { //nolint:errorlint // explicitly returned
oc.SetSpanStatus(span, hr)
}
}()

View File

@@ -1,3 +1,5 @@
//go:build windows
package wclayer
import (
@@ -64,7 +66,7 @@ func (r *baseLayerReader) walkUntilCancelled() error {
return nil
})
if err == errorIterationCanceled {
if err == errorIterationCanceled { //nolint:errorlint // explicitly returned
return nil
}
@@ -103,7 +105,7 @@ func (r *baseLayerReader) walkUntilCancelled() error {
return nil
})
if err == errorIterationCanceled {
if err == errorIterationCanceled { //nolint:errorlint // explicitly returned
return nil
}

View File

@@ -0,0 +1,289 @@
//go:build windows
package cim
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/Microsoft/go-winio"
"github.com/Microsoft/hcsshim/internal/log"
"github.com/Microsoft/hcsshim/internal/oc"
"github.com/Microsoft/hcsshim/internal/wclayer"
"github.com/Microsoft/hcsshim/osversion"
"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.
path 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
}
type hive struct {
name string
base string
delta string
}
var (
hives = []hive{
{"SYSTEM", "SYSTEM_BASE", "SYSTEM_DELTA"},
{"SOFTWARE", "SOFTWARE_BASE", "SOFTWARE_DELTA"},
{"SAM", "SAM_BASE", "SAM_DELTA"},
{"SECURITY", "SECURITY_BASE", "SECURITY_DELTA"},
{"DEFAULT", "DEFAULTUSER_BASE", "DEFAULTUSER_DELTA"},
}
)
func isDeltaOrBaseHive(path string) bool {
for _, hv := range hives {
if strings.EqualFold(path, filepath.Join(wclayer.HivesPath, hv.delta)) ||
strings.EqualFold(path, filepath.Join(wclayer.RegFilesPath, hv.name)) {
return true
}
}
return false
}
// checks if this particular file should be written with a stdFileWriter instead of
// using the cimWriter.
func isStdFile(path string) bool {
return (isDeltaOrBaseHive(path) ||
path == filepath.Join(wclayer.UtilityVMPath, wclayer.RegFilesPath, "SYSTEM") ||
path == filepath.Join(wclayer.UtilityVMPath, wclayer.RegFilesPath, "SOFTWARE") ||
path == wclayer.BcdFilePath || path == wclayer.BootMgrFilePath)
}
// 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 {
if name == wclayer.UtilityVMPath {
cw.hasUtilityVM = true
}
if isStdFile(name) {
// create a pending op for this file
cw.pendingOps = append(cw.pendingOps, &addOp{
pathInCim: name,
hostPath: filepath.Join(cw.path, name),
fileInfo: fileInfo,
securityDescriptor: securityDescriptor,
extendedAttributes: extendedAttributes,
reparseData: reparseData,
})
if err := cw.stdFileWriter.Add(name); err != nil {
return err
}
cw.activeWriter = cw.stdFileWriter
} else {
if err := cw.cimWriter.AddFile(name, fileInfo, fileSize, securityDescriptor, extendedAttributes, reparseData); err != nil {
return err
}
cw.activeWriter = cw.cimWriter
}
return nil
}
// AddLink adds a hard link to the layer. The target must already have been added.
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) {
// If this is a link to a std file it will have to be added later once the
// std file is written to the CIM. Create a pending op for this
cw.pendingOps = append(cw.pendingOps, &linkOp{
oldPath: target,
newPath: name,
})
return nil
} else if isStdFile(name) {
// None of the predefined std files are links. If they show up as links this is unexpected
// behavior. Error out.
return fmt.Errorf("unexpected link %s in layer", name)
} else {
return cw.cimWriter.AddLink(target, name)
}
}
// 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 {
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.
return fmt.Errorf("unexpected alternate stream %s in layer", name)
}
if err := cw.cimWriter.CreateAlternateStream(name, size); err != nil {
return err
}
cw.activeWriter = cw.cimWriter
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) {
return cw.activeWriter.Write(b)
}
// Close finishes the layer writing process and releases any resources.
func (cw *CimLayerWriter) Close(ctx context.Context) (retErr error) {
if err := cw.stdFileWriter.Close(ctx); err != nil {
return err
}
// cimWriter must be closed even if there are errors.
defer func() {
if err := cw.cimWriter.Close(); retErr == nil {
retErr = err
}
}()
// Find out the osversion of this layer, both base & non-base layers can have UtilityVM layer.
processUtilityVM := false
if cw.hasUtilityVM {
uvmSoftwareHivePath := filepath.Join(cw.path, wclayer.UtilityVMPath, wclayer.RegFilesPath, "SOFTWARE")
osvStr, err := getOsBuildNumberFromRegistry(uvmSoftwareHivePath)
if err != nil {
return fmt.Errorf("read os version string from UtilityVM SOFTWARE hive: %w", err)
}
osv, err := strconv.ParseUint(osvStr, 10, 16)
if err != nil {
return fmt.Errorf("parse os version string (%s): %w", osvStr, err)
}
// write this version to a file for future reference by the shim process
if err = wclayer.WriteLayerUvmBuildFile(cw.path, uint16(osv)); err != nil {
return fmt.Errorf("write uvm build version: %w", err)
}
// CIMFS for hyperV isolated is only supported after 20348, processing UtilityVM layer on 2048
// & lower will cause failures since those images won't have CIMFS specific UVM files (mostly
// BCD entries required for CIMFS)
processUtilityVM = (osv > osversion.LTSC2022)
log.G(ctx).Debugf("import image os version %d, processing UtilityVM layer: %t\n", osv, processUtilityVM)
}
if len(cw.parentLayerPaths) == 0 {
if err := cw.processBaseLayer(ctx, processUtilityVM); err != nil {
return fmt.Errorf("process base layer: %w", err)
}
} else {
if err := cw.processNonBaseLayer(ctx, processUtilityVM); err != nil {
return fmt.Errorf("process non base layer: %w", err)
}
}
for _, op := range cw.pendingOps {
if err := op.apply(cw.cimWriter); err != nil {
return fmt.Errorf("apply pending operations: %w", err)
}
}
return nil
}
func NewCimLayerWriter(ctx context.Context, path string, parentLayerPaths []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", path),
trace.StringAttribute("parentLayerPaths", strings.Join(parentLayerPaths, ", ")))
parentCim := ""
cimDirPath := GetCimDirFromLayer(path)
if _, err = os.Stat(cimDirPath); os.IsNotExist(err) {
// create cim directory
if err = os.Mkdir(cimDirPath, 0755); err != nil {
return nil, fmt.Errorf("failed while creating cim layers directory: %w", err)
}
} else if err != nil {
return nil, fmt.Errorf("unable to access cim layers directory: %w", err)
}
if len(parentLayerPaths) > 0 {
parentCim = GetCimNameFromLayer(parentLayerPaths[0])
}
cim, err := cimfs.Create(cimDirPath, parentCim, GetCimNameFromLayer(path))
if err != nil {
return nil, fmt.Errorf("error in creating a new cim: %w", err)
}
sfw, err := newStdFileWriter(path, parentLayerPaths)
if err != nil {
return nil, fmt.Errorf("error in creating new standard file writer: %w", err)
}
return &CimLayerWriter{
ctx: ctx,
s: span,
path: path,
parentLayerPaths: parentLayerPaths,
cimWriter: cim,
stdFileWriter: sfw,
}, nil
}
// DestroyCimLayer destroys a cim layer i.e it removes all the cimfs files for the given layer as well as
// all of the other files that are stored in the layer directory (at path `layerPath`).
// If this is not a cimfs layer (i.e a cim file for the given layer does not exist) then nothing is done.
func DestroyCimLayer(ctx context.Context, layerPath string) error {
cimPath := GetCimPathFromLayer(layerPath)
// verify that such a cim exists first, sometimes containerd tries to call
// this with the root snapshot directory as the layer path. We don't want to
// destroy everything inside the snapshots directory.
if _, err := os.Stat(cimPath); err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
return cimfs.DestroyCim(ctx, cimPath)
}

View File

@@ -0,0 +1,107 @@
//go:build windows
package cim
import (
"bytes"
"fmt"
"os/exec"
"github.com/Microsoft/go-winio/pkg/guid"
)
const (
bcdFilePath = "UtilityVM\\Files\\EFI\\Microsoft\\Boot\\BCD"
cimfsDeviceOptionsID = "{763e9fea-502d-434f-aad9-5fabe9c91a7b}"
vmbusDeviceID = "{c63c9bdf-5fa5-4208-b03f-6b458b365592}"
compositeDeviceOptionsID = "{e1787220-d17f-49e7-977a-d8fe4c8537e2}"
bootContainerID = "{b890454c-80de-4e98-a7ab-56b74b4fbd0c}"
)
func bcdExec(storePath string, args ...string) error {
var out bytes.Buffer
argsArr := []string{"/store", storePath, "/offline"}
argsArr = append(argsArr, args...)
cmd := exec.Command("bcdedit.exe", argsArr...)
cmd.Stdout = &out
if err := cmd.Run(); err != nil {
return fmt.Errorf("bcd command (%s) failed: %w", cmd, err)
}
return nil
}
// A registry configuration required for the uvm.
func setBcdRestartOnFailure(storePath string) error {
return bcdExec(storePath, "/set", "{default}", "restartonfailure", "yes")
}
func setBcdCimBootDevice(storePath, cimPathRelativeToVSMB string, diskID, partitionID guid.GUID) error {
// create options for cimfs boot device
if err := bcdExec(storePath, "/create", cimfsDeviceOptionsID, "/d", "CimFS Device Options", "/device"); err != nil {
return err
}
// Set options. For now we need to set 2 options. First is the parent device i.e the device under
// which all cim files will be available. Second is the path of the cim (from which this UVM should
// boot) relative to the parent device. Note that even though the 2nd option is named
// `cimfsrootdirectory` it expects a path to the cim file and not a directory path.
if err := bcdExec(storePath, "/set", cimfsDeviceOptionsID, "cimfsparentdevice", fmt.Sprintf("vmbus=%s", vmbusDeviceID)); err != nil {
return err
}
if err := bcdExec(storePath, "/set", cimfsDeviceOptionsID, "cimfsrootdirectory", fmt.Sprintf("\\%s", cimPathRelativeToVSMB)); err != nil {
return err
}
// create options for the composite device
if err := bcdExec(storePath, "/create", compositeDeviceOptionsID, "/d", "Composite Device Options", "/device"); err != nil {
return err
}
// We need to specify the diskID & the partition ID of the boot disk and we need to set the cimfs boot
// options ID
partitionStr := fmt.Sprintf("gpt_partition={%s};{%s}", diskID, partitionID)
if err := bcdExec(storePath, "/set", compositeDeviceOptionsID, "primarydevice", partitionStr); err != nil {
return err
}
if err := bcdExec(storePath, "/set", compositeDeviceOptionsID, "secondarydevice", fmt.Sprintf("cimfs=%s,%s", bootContainerID, cimfsDeviceOptionsID)); err != nil {
return err
}
if err := bcdExec(storePath, "/set", "{default}", "device", fmt.Sprintf("composite=0,%s", compositeDeviceOptionsID)); err != nil {
return err
}
if err := bcdExec(storePath, "/set", "{default}", "osdevice", fmt.Sprintf("composite=0,%s", compositeDeviceOptionsID)); err != nil {
return err
}
// Since our UVM file are stored under UtilityVM\Files directory inside the CIM we must prepend that
// directory in front of paths used by bootmgr
if err := bcdExec(storePath, "/set", "{default}", "path", "\\UtilityVM\\Files\\Windows\\System32\\winload.efi"); err != nil {
return err
}
if err := bcdExec(storePath, "/set", "{default}", "systemroot", "\\UtilityVM\\Files\\Windows"); err != nil {
return err
}
return nil
}
// updateBcdStoreForBoot Updates the bcd store at path layerPath + UtilityVM\Files\EFI\Microsoft\Boot\BCD` to
// boot with the disk with given ID and given partitionID. cimPathRelativeToVSMB is the path of the cim which
// will be used for booting this UVM relative to the VSMB share. (Usually, the entire snapshots directory will
// be shared over VSMB, so if this is the cim-layers\1.cim under that directory, the value of
// `cimPathRelativeToVSMB` should be cim-layers\1.cim)
func updateBcdStoreForBoot(storePath string, cimPathRelativeToVSMB string, diskID, partitionID guid.GUID) error {
if err := setBcdRestartOnFailure(storePath); err != nil {
return err
}
if err := setBcdCimBootDevice(storePath, cimPathRelativeToVSMB, diskID, partitionID); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,41 @@
//go:build windows
package cim
import (
"os"
"path/filepath"
)
const (
// name of the directory in which cims are stored
cimDir = "cim-layers"
)
// Usually layers are stored at ./root/io.containerd.snapshotter.v1.windows/snapshots/<layerid>. For cimfs we
// must store all layer cims in the same directory (for forked cims to work). So all cim layers are stored in
// /root/io.containerd.snapshotter.v1.windows/snapshots/cim-layers. And the cim file representing each
// individual layer is stored at /root/io.containerd.snapshotter.v1.windows/snapshots/cim-layers/<layerid>.cim
// CimName is the filename (<layerid>.cim) of the file representing the cim
func GetCimNameFromLayer(layerPath string) string {
return filepath.Base(layerPath) + ".cim"
}
// CimPath is the path to the CimDir/<layerid>.cim file that represents a layer cim.
func GetCimPathFromLayer(layerPath string) string {
return filepath.Join(GetCimDirFromLayer(layerPath), GetCimNameFromLayer(layerPath))
}
// CimDir is the directory inside which all cims are stored.
func GetCimDirFromLayer(layerPath string) string {
dir := filepath.Dir(layerPath)
return filepath.Join(dir, cimDir)
}
// IsCimLayer returns `true` if the layer at path `layerPath` is a cim layer. Returns `false` otherwise.
func IsCimLayer(layerPath string) bool {
cimPath := GetCimPathFromLayer(layerPath)
_, err := os.Stat(cimPath)
return (err == nil)
}

View File

@@ -0,0 +1,3 @@
// This package provides utilities for working with container image layers in the cim format
// via the wclayer APIs.
package cim

View File

@@ -0,0 +1,90 @@
//go:build windows
package cim
import (
"context"
"fmt"
"os"
"path/filepath"
"syscall"
"github.com/Microsoft/go-winio"
"github.com/Microsoft/hcsshim/internal/safefile"
"github.com/Microsoft/hcsshim/internal/winapi"
)
// stdFileWriter writes the files of a layer to the layer folder instead of writing them inside the cim.
// For some files (like the Hive files or some UtilityVM files) it is necessary to write them as a normal file
// first, do some modifications on them (for example merging of hives or processing of UtilityVM files)
// and then write the modified versions into the cim. This writer is used for such files.
type stdFileWriter struct {
activeFile *os.File
// parent layer paths
parentLayerPaths []string
// path to the current layer
path string
// the open handle to the path directory
root *os.File
}
func newStdFileWriter(root string, parentRoots []string) (sfw *stdFileWriter, err error) {
sfw = &stdFileWriter{
path: root,
parentLayerPaths: parentRoots,
}
sfw.root, err = safefile.OpenRoot(root)
if err != nil {
return
}
return
}
func (sfw *stdFileWriter) closeActiveFile() (err error) {
if sfw.activeFile != nil {
err = sfw.activeFile.Close()
sfw.activeFile = nil
}
return
}
// Adds a new file or an alternate data stream to an existing file inside the layer directory.
func (sfw *stdFileWriter) Add(name string) error {
if err := sfw.closeActiveFile(); err != nil {
return err
}
// The directory of this file might be created inside the cim.
// make sure we have the same parent directory chain here
if err := safefile.MkdirAllRelative(filepath.Dir(name), sfw.root); err != nil {
return fmt.Errorf("failed to create file %s: %w", name, err)
}
f, err := safefile.OpenRelative(
name,
sfw.root,
syscall.GENERIC_READ|syscall.GENERIC_WRITE|winio.WRITE_DAC|winio.WRITE_OWNER,
syscall.FILE_SHARE_READ,
winapi.FILE_CREATE,
0,
)
if err != nil {
return fmt.Errorf("error creating file %s: %w", name, err)
}
sfw.activeFile = f
return nil
}
// Write writes data to the current file. The data must be in the format of a Win32
// backup stream.
func (sfw *stdFileWriter) Write(b []byte) (int, error) {
return sfw.activeFile.Write(b)
}
// Close finishes the layer writing process and releases any resources.
func (sfw *stdFileWriter) Close(ctx context.Context) error {
if err := sfw.closeActiveFile(); err != nil {
return fmt.Errorf("failed to close active file %s : %w", sfw.activeFile.Name(), err)
}
return nil
}

View File

@@ -0,0 +1,89 @@
//go:build windows
package cim
import (
"context"
"fmt"
"os"
"sync"
"github.com/Microsoft/go-winio/pkg/guid"
hcsschema "github.com/Microsoft/hcsshim/internal/hcs/schema2"
cimfs "github.com/Microsoft/hcsshim/pkg/cimfs"
)
// a cache of cim layer to its mounted volume - The mount manager plugin currently doesn't have an option of
// querying a mounted cim to get the volume at which it is mounted, so we maintain a cache of that here
var (
cimMounts map[string]string = make(map[string]string)
cimMountMapLock sync.Mutex
// A random GUID used as a namespace for generating cim mount volume GUIDs: 6827367b-c388-4e9b-95ec-961c6d2c936c
cimMountNamespace guid.GUID = guid.GUID{Data1: 0x6827367b, Data2: 0xc388, Data3: 0x4e9b, Data4: [8]byte{0x96, 0x1c, 0x6d, 0x2c, 0x93, 0x6c}}
)
// MountCimLayer mounts the cim at path `cimPath` and returns the mount location of that cim. This method
// uses the `CimMountFlagCacheFiles` mount flag when mounting the cim. The containerID is used to generated
// the volumeID for the volume at which this CIM is mounted. containerID is used so that if the shim process
// crashes for any reason, the mounted cim can be correctly cleaned up during `shim delete` call.
func MountCimLayer(ctx context.Context, cimPath, containerID string) (string, error) {
volumeGUID, err := guid.NewV5(cimMountNamespace, []byte(containerID))
if err != nil {
return "", fmt.Errorf("generated cim mount GUID: %w", err)
}
vol, err := cimfs.Mount(cimPath, volumeGUID, hcsschema.CimMountFlagCacheFiles)
if err != nil {
return "", err
}
cimMountMapLock.Lock()
defer cimMountMapLock.Unlock()
cimMounts[fmt.Sprintf("%s_%s", containerID, cimPath)] = vol
return vol, nil
}
// Unmount unmounts the cim at mounted for given container.
func UnmountCimLayer(ctx context.Context, cimPath, containerID string) error {
cimMountMapLock.Lock()
defer cimMountMapLock.Unlock()
if vol, ok := cimMounts[fmt.Sprintf("%s_%s", containerID, cimPath)]; !ok {
return fmt.Errorf("cim %s not mounted", cimPath)
} else {
delete(cimMounts, fmt.Sprintf("%s_%s", containerID, cimPath))
err := cimfs.Unmount(vol)
if err != nil {
return err
}
}
return nil
}
// GetCimMountPath returns the volume at which a cim is mounted. If the cim is not mounted returns error
func GetCimMountPath(cimPath, containerID string) (string, error) {
cimMountMapLock.Lock()
defer cimMountMapLock.Unlock()
if vol, ok := cimMounts[fmt.Sprintf("%s_%s", containerID, cimPath)]; !ok {
return "", fmt.Errorf("cim %s not mounted", cimPath)
} else {
return vol, nil
}
}
func CleanupContainerMounts(containerID string) error {
volumeGUID, err := guid.NewV5(cimMountNamespace, []byte(containerID))
if err != nil {
return fmt.Errorf("generated cim mount GUID: %w", err)
}
volPath := fmt.Sprintf("\\\\?\\Volume{%s}\\", volumeGUID.String())
if _, err := os.Stat(volPath); err == nil {
err = cimfs.Unmount(volPath)
if err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,68 @@
//go:build windows
package cim
import (
"fmt"
"io"
"os"
"github.com/Microsoft/go-winio"
"github.com/Microsoft/hcsshim/pkg/cimfs"
"golang.org/x/sys/windows"
)
type pendingCimOp interface {
apply(cw *cimfs.CimFsWriter) error
}
// add op represents a pending operation of adding a new file inside the cim
type addOp struct {
// path inside the cim at which the file should be added
pathInCim string
// host path where this file was temporarily written.
hostPath string
// other file metadata fields that were provided during the add call.
fileInfo *winio.FileBasicInfo
securityDescriptor []byte
extendedAttributes []byte
reparseData []byte
}
func (o *addOp) apply(cw *cimfs.CimFsWriter) error {
f, err := os.Open(o.hostPath)
if err != nil {
return fmt.Errorf("open file %s: %w", o.hostPath, err)
}
defer f.Close()
fs, err := f.Stat()
if err != nil {
return fmt.Errorf("stat file %s: %w", o.hostPath, err)
}
if err := cw.AddFile(o.pathInCim, o.fileInfo, fs.Size(), o.securityDescriptor, o.extendedAttributes, o.reparseData); err != nil {
return fmt.Errorf("cim add file %s: %w", o.hostPath, err)
}
if o.fileInfo.FileAttributes != windows.FILE_ATTRIBUTE_DIRECTORY {
written, err := io.Copy(cw, f)
if err != nil {
return fmt.Errorf("write file %s inside cim: %w", o.hostPath, err)
} else if written != fs.Size() {
return fmt.Errorf("short write to cim for file %s, expected %d bytes wrote %d", o.hostPath, fs.Size(), written)
}
}
return nil
}
// linkOp represents a pending link file operation inside the cim
type linkOp struct {
// old & new paths inside the cim where the link should be created
oldPath string
newPath string
}
func (o *linkOp) apply(cw *cimfs.CimFsWriter) error {
return cw.AddLink(o.oldPath, o.newPath)
}

View File

@@ -0,0 +1,293 @@
//go:build windows
package cim
import (
"context"
"fmt"
"os"
"path/filepath"
"syscall"
"time"
"github.com/Microsoft/go-winio"
"github.com/Microsoft/go-winio/vhd"
"github.com/Microsoft/hcsshim/computestorage"
"github.com/Microsoft/hcsshim/internal/memory"
"github.com/Microsoft/hcsshim/internal/security"
"github.com/Microsoft/hcsshim/internal/vhdx"
"github.com/Microsoft/hcsshim/internal/wclayer"
"golang.org/x/sys/windows"
)
const defaultVHDXBlockSizeInMB = 1
func createContainerBaseLayerVHDs(ctx context.Context, layerPath string) (err error) {
baseVhdPath := filepath.Join(layerPath, wclayer.ContainerBaseVhd)
diffVhdPath := filepath.Join(layerPath, wclayer.ContainerScratchVhd)
defaultVhdSize := uint64(20)
if _, err := os.Stat(baseVhdPath); err == nil {
if err := os.RemoveAll(baseVhdPath); err != nil {
return fmt.Errorf("failed to remove base vhdx path: %w", err)
}
}
if _, err := os.Stat(diffVhdPath); err == nil {
if err := os.RemoveAll(diffVhdPath); err != nil {
return fmt.Errorf("failed to remove differencing vhdx: %w", err)
}
}
createParams := &vhd.CreateVirtualDiskParameters{
Version: 2,
Version2: vhd.CreateVersion2{
MaximumSize: defaultVhdSize * memory.GiB,
BlockSizeInBytes: defaultVHDXBlockSizeInMB * memory.MiB,
},
}
handle, err := vhd.CreateVirtualDisk(baseVhdPath, vhd.VirtualDiskAccessNone, vhd.CreateVirtualDiskFlagNone, createParams)
if err != nil {
return fmt.Errorf("failed to create vhdx: %w", err)
}
defer func() {
if err != nil {
os.RemoveAll(baseVhdPath)
os.RemoveAll(diffVhdPath)
}
}()
err = computestorage.FormatWritableLayerVhd(ctx, windows.Handle(handle))
// we always wanna close the handle whether format succeeds for not.
closeErr := syscall.CloseHandle(handle)
if err != nil {
return err
} else if closeErr != nil {
return fmt.Errorf("failed to close vhdx handle: %w", closeErr)
}
// Create the differencing disk that will be what's copied for the final rw layer
// for a container.
if err = vhd.CreateDiffVhd(diffVhdPath, baseVhdPath, defaultVHDXBlockSizeInMB); err != nil {
return fmt.Errorf("failed to create differencing disk: %w", err)
}
if err = security.GrantVmGroupAccess(baseVhdPath); err != nil {
return fmt.Errorf("failed to grant vm group access to %s: %w", baseVhdPath, err)
}
if err = security.GrantVmGroupAccess(diffVhdPath); err != nil {
return fmt.Errorf("failed to grant vm group access to %s: %w", diffVhdPath, err)
}
return nil
}
// processUtilityVMLayer is similar to createContainerBaseLayerVHDs but along with the scratch creation it
// also does some BCD modifications to allow the UVM to boot from the CIM. It expects that the UVM BCD file is
// present at layerPath/`wclayer.BcdFilePath` and a UVM SYSTEM hive is present at
// layerPath/UtilityVM/`wclayer.RegFilesPath`/SYSTEM. The scratch VHDs are created under the `layerPath`
// directory.
func processUtilityVMLayer(ctx context.Context, layerPath string) error {
// func createUtilityVMLayerVHDs(ctx context.Context, layerPath string) error {
baseVhdPath := filepath.Join(layerPath, wclayer.UtilityVMPath, wclayer.UtilityVMBaseVhd)
diffVhdPath := filepath.Join(layerPath, wclayer.UtilityVMPath, wclayer.UtilityVMScratchVhd)
defaultVhdSize := uint64(10)
// Just create the vhdx for utilityVM layer, no need to format it.
createParams := &vhd.CreateVirtualDiskParameters{
Version: 2,
Version2: vhd.CreateVersion2{
MaximumSize: defaultVhdSize * memory.GiB,
BlockSizeInBytes: defaultVHDXBlockSizeInMB * memory.MiB,
},
}
handle, err := vhd.CreateVirtualDisk(baseVhdPath, vhd.VirtualDiskAccessNone, vhd.CreateVirtualDiskFlagNone, createParams)
if err != nil {
return fmt.Errorf("failed to create vhdx: %w", err)
}
defer func() {
if err != nil {
os.RemoveAll(baseVhdPath)
os.RemoveAll(diffVhdPath)
}
}()
err = computestorage.FormatWritableLayerVhd(ctx, windows.Handle(handle))
closeErr := syscall.CloseHandle(handle)
if err != nil {
return err
} else if closeErr != nil {
return fmt.Errorf("failed to close vhdx handle: %w", closeErr)
}
partitionInfo, err := vhdx.GetScratchVhdPartitionInfo(ctx, baseVhdPath)
if err != nil {
return fmt.Errorf("failed to get base vhd layout info: %w", err)
}
// relativeCimPath needs to be the cim path relative to the snapshots directory. The snapshots
// directory is shared inside the UVM over VSMB, so during the UVM boot this relative path will be
// used to find the cim file under that VSMB share.
relativeCimPath := filepath.Join(filepath.Base(GetCimDirFromLayer(layerPath)), GetCimNameFromLayer(layerPath))
bcdPath := filepath.Join(layerPath, bcdFilePath)
if err = updateBcdStoreForBoot(bcdPath, relativeCimPath, partitionInfo.DiskID, partitionInfo.PartitionID); err != nil {
return fmt.Errorf("failed to update BCD: %w", err)
}
if err := enableCimBoot(filepath.Join(layerPath, wclayer.UtilityVMPath, wclayer.RegFilesPath, "SYSTEM")); err != nil {
return fmt.Errorf("failed to setup cim image for uvm boot: %w", err)
}
// Note: diff vhd creation and granting of vm group access must be done AFTER
// getting the partition info of the base VHD. Otherwise it causes the vhd parent
// chain to get corrupted.
// TODO(ambarve): figure out why this happens so that bcd update can be moved to a separate function
// Create the differencing disk that will be what's copied for the final rw layer
// for a container.
if err = vhd.CreateDiffVhd(diffVhdPath, baseVhdPath, defaultVHDXBlockSizeInMB); err != nil {
return fmt.Errorf("failed to create differencing disk: %w", err)
}
if err := security.GrantVmGroupAccess(baseVhdPath); err != nil {
return fmt.Errorf("failed to grant vm group access to %s: %w", baseVhdPath, err)
}
if err := security.GrantVmGroupAccess(diffVhdPath); err != nil {
return fmt.Errorf("failed to grant vm group access to %s: %w", diffVhdPath, err)
}
return nil
}
// processBaseLayerHives make the base layer specific modifications on the hives and emits equivalent the
// pendingCimOps that should be applied on the CIM. In base layer we need to create hard links from registry
// hives under Files/Windows/Sysetm32/config into Hives/*_BASE. This function creates these links outside so
// that the registry hives under Hives/ are available during children layers import. Then we write these hive
// files inside the cim and create links inside the cim.
func processBaseLayerHives(layerPath string) ([]pendingCimOp, error) {
pendingOps := []pendingCimOp{}
// make hives directory both outside and in the cim
if err := os.Mkdir(filepath.Join(layerPath, wclayer.HivesPath), 0755); err != nil {
return pendingOps, fmt.Errorf("hives directory creation: %w", err)
}
hivesDirInfo := &winio.FileBasicInfo{
CreationTime: windows.NsecToFiletime(time.Now().UnixNano()),
LastAccessTime: windows.NsecToFiletime(time.Now().UnixNano()),
LastWriteTime: windows.NsecToFiletime(time.Now().UnixNano()),
ChangeTime: windows.NsecToFiletime(time.Now().UnixNano()),
FileAttributes: windows.FILE_ATTRIBUTE_DIRECTORY,
}
pendingOps = append(pendingOps, &addOp{
pathInCim: wclayer.HivesPath,
hostPath: filepath.Join(layerPath, wclayer.HivesPath),
fileInfo: hivesDirInfo,
})
// add hard links from base hive files.
for _, hv := range hives {
oldHivePathRelative := filepath.Join(wclayer.RegFilesPath, hv.name)
newHivePathRelative := filepath.Join(wclayer.HivesPath, hv.base)
if err := os.Link(filepath.Join(layerPath, oldHivePathRelative), filepath.Join(layerPath, newHivePathRelative)); err != nil {
return pendingOps, fmt.Errorf("hive link creation: %w", err)
}
pendingOps = append(pendingOps, &linkOp{
oldPath: oldHivePathRelative,
newPath: newHivePathRelative,
})
}
return pendingOps, nil
}
// processLayoutFile creates a file named "layout" in the root of the base layer. This allows certain
// container startup related functions to understand that the hives are a part of the container rootfs.
func processLayoutFile(layerPath string) ([]pendingCimOp, error) {
fileContents := "vhd-with-hives\n"
if err := os.WriteFile(filepath.Join(layerPath, "layout"), []byte(fileContents), 0755); err != nil {
return []pendingCimOp{}, fmt.Errorf("write layout file: %w", err)
}
layoutFileInfo := &winio.FileBasicInfo{
CreationTime: windows.NsecToFiletime(time.Now().UnixNano()),
LastAccessTime: windows.NsecToFiletime(time.Now().UnixNano()),
LastWriteTime: windows.NsecToFiletime(time.Now().UnixNano()),
ChangeTime: windows.NsecToFiletime(time.Now().UnixNano()),
FileAttributes: windows.FILE_ATTRIBUTE_NORMAL,
}
op := &addOp{
pathInCim: "layout",
hostPath: filepath.Join(layerPath, "layout"),
fileInfo: layoutFileInfo,
}
return []pendingCimOp{op}, nil
}
// Some of the layer files that are generated during the processBaseLayer call must be added back
// inside the cim, some registry file links must be updated. This function takes care of all those
// steps. This function opens the cim file for writing and updates it.
func (cw *CimLayerWriter) processBaseLayer(ctx context.Context, processUtilityVM bool) (err error) {
if err = createContainerBaseLayerVHDs(ctx, cw.path); err != nil {
return fmt.Errorf("failed to create container base VHDs: %w", err)
}
if processUtilityVM {
if err = processUtilityVMLayer(ctx, cw.path); err != nil {
return fmt.Errorf("process utilityVM layer: %w", err)
}
}
ops, err := processBaseLayerHives(cw.path)
if err != nil {
return err
}
cw.pendingOps = append(cw.pendingOps, ops...)
ops, err = processLayoutFile(cw.path)
if err != nil {
return err
}
cw.pendingOps = append(cw.pendingOps, ops...)
return nil
}
// processNonBaseLayer takes care of the processing required for a non base layer. As of now
// the only processing required for non base layer is to merge the delta registry hives of the
// non-base layer with it's parent layer.
func (cw *CimLayerWriter) processNonBaseLayer(ctx context.Context, processUtilityVM bool) (err error) {
for _, hv := range hives {
baseHive := filepath.Join(wclayer.HivesPath, hv.base)
deltaHive := filepath.Join(wclayer.HivesPath, hv.delta)
_, err := os.Stat(filepath.Join(cw.path, deltaHive))
// merge with parent layer if delta exists.
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("stat delta hive %s: %w", filepath.Join(cw.path, deltaHive), err)
} else if err == nil {
// merge base hive of parent layer with the delta hive of this layer and write it as
// the base hive of this layer.
err = mergeHive(filepath.Join(cw.parentLayerPaths[0], baseHive), filepath.Join(cw.path, deltaHive), filepath.Join(cw.path, baseHive))
if err != nil {
return err
}
// the newly created merged file must be added to the cim
cw.pendingOps = append(cw.pendingOps, &addOp{
pathInCim: baseHive,
hostPath: filepath.Join(cw.path, baseHive),
fileInfo: &winio.FileBasicInfo{
CreationTime: windows.NsecToFiletime(time.Now().UnixNano()),
LastAccessTime: windows.NsecToFiletime(time.Now().UnixNano()),
LastWriteTime: windows.NsecToFiletime(time.Now().UnixNano()),
ChangeTime: windows.NsecToFiletime(time.Now().UnixNano()),
FileAttributes: windows.FILE_ATTRIBUTE_NORMAL,
},
})
}
}
if processUtilityVM {
return processUtilityVMLayer(ctx, cw.path)
}
return nil
}

View File

@@ -0,0 +1,172 @@
//go:build windows
package cim
import (
"encoding/binary"
"fmt"
"os"
"unsafe"
"github.com/Microsoft/hcsshim/internal/log"
"github.com/Microsoft/hcsshim/internal/winapi"
"github.com/Microsoft/hcsshim/osversion"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/sys/windows"
)
// enableCimBoot Opens the SYSTEM registry hive at path `hivePath` and updates it to include a CIMFS Start
// registry key. This prepares the uvm to boot from a cim file if requested. The registry changes required to
// actually make the uvm boot from a cim will be added in the uvm config (look at
// addBootFromCimRegistryChanges for details). This registry key needs to be available in the early boot
// phase and so including it in the uvm config doesn't work.
func enableCimBoot(hivePath string) (err error) {
dataZero := make([]byte, 4)
dataOne := make([]byte, 4)
binary.LittleEndian.PutUint32(dataOne, 1)
dataFour := make([]byte, 4)
binary.LittleEndian.PutUint32(dataFour, 4)
bootGUID, err := windows.UTF16FromString(bootContainerID)
if err != nil {
return fmt.Errorf("failed to encode boot guid to utf16: %w", err)
}
overrideBootPath, err := windows.UTF16FromString("\\Windows\\")
if err != nil {
return fmt.Errorf("failed to encode override boot path to utf16: %w", err)
}
regChanges := []struct {
keyPath string
valueName string
valueType winapi.RegType
data *byte
dataLen uint32
}{
{"ControlSet001\\Control", "BootContainerGuid", winapi.REG_TYPE_SZ, (*byte)(unsafe.Pointer(&bootGUID[0])), 2 * uint32(len(bootGUID))},
{"ControlSet001\\Services\\UnionFS", "Start", winapi.REG_TYPE_DWORD, &dataZero[0], uint32(len(dataZero))},
{"ControlSet001\\Services\\wcifs", "Start", winapi.REG_TYPE_DWORD, &dataFour[0], uint32(len(dataZero))},
// The bootmgr loads the uvm files from the cim and so uses the relative path `UtilityVM\\Files` inside the cim to access the uvm files. However, once the cim is mounted UnionFS will merge the correct directory (UtilityVM\\Files) of the cim with the scratch and then that point onwards we don't need to use the relative path. Below registry key tells the kernel that the boot path that was provided in BCD should now be overriden with this new path.
{"Setup", "BootPathOverride", winapi.REG_TYPE_SZ, (*byte)(unsafe.Pointer(&overrideBootPath[0])), 2 * uint32(len(overrideBootPath))},
}
var storeHandle winapi.ORHKey
if err = winapi.OROpenHive(hivePath, &storeHandle); err != nil {
return fmt.Errorf("failed to open registry store at %s: %w", hivePath, err)
}
for _, change := range regChanges {
var changeKey winapi.ORHKey
if err = winapi.ORCreateKey(storeHandle, change.keyPath, 0, 0, 0, &changeKey, nil); err != nil {
return fmt.Errorf("failed to open reg key %s: %w", change.keyPath, err)
}
if err = winapi.ORSetValue(changeKey, change.valueName, uint32(change.valueType), change.data, change.dataLen); err != nil {
return fmt.Errorf("failed to set value for regkey %s\\%s : %w", change.keyPath, change.valueName, err)
}
}
// remove the existing file first
if err := os.Remove(hivePath); err != nil {
return fmt.Errorf("failed to remove existing registry %s: %w", hivePath, err)
}
if err = winapi.ORSaveHive(winapi.ORHKey(storeHandle), hivePath, uint32(osversion.Get().MajorVersion), uint32(osversion.Get().MinorVersion)); err != nil {
return fmt.Errorf("error saving the registry store: %w", err)
}
// close hive irrespective of the errors
if err := winapi.ORCloseHive(winapi.ORHKey(storeHandle)); err != nil {
return fmt.Errorf("error closing registry store; %w", err)
}
return nil
}
// mergeHive merges the hive located at parentHivePath with the hive located at deltaHivePath and stores
// the result into the file at mergedHivePath. If a file already exists at path `mergedHivePath` then it
// throws an error.
func mergeHive(parentHivePath, deltaHivePath, mergedHivePath string) (err error) {
var baseHive, deltaHive, mergedHive winapi.ORHKey
if err := winapi.OROpenHive(parentHivePath, &baseHive); err != nil {
return fmt.Errorf("failed to open base hive %s: %w", parentHivePath, err)
}
defer func() {
err2 := winapi.ORCloseHive(baseHive)
if err == nil {
err = errors.Wrap(err2, "failed to close base hive")
}
}()
if err := winapi.OROpenHive(deltaHivePath, &deltaHive); err != nil {
return fmt.Errorf("failed to open delta hive %s: %w", deltaHivePath, err)
}
defer func() {
err2 := winapi.ORCloseHive(deltaHive)
if err == nil {
err = errors.Wrap(err2, "failed to close delta hive")
}
}()
if err := winapi.ORMergeHives([]winapi.ORHKey{baseHive, deltaHive}, &mergedHive); err != nil {
return fmt.Errorf("failed to merge hives: %w", err)
}
defer func() {
err2 := winapi.ORCloseHive(mergedHive)
if err == nil {
err = errors.Wrap(err2, "failed to close merged hive")
}
}()
if err := winapi.ORSaveHive(mergedHive, mergedHivePath, uint32(osversion.Get().MajorVersion), uint32(osversion.Get().MinorVersion)); err != nil {
return fmt.Errorf("failed to save hive: %w", err)
}
return
}
// getOsBuildNumberFromRegistry fetches the "CurrentBuild" value at path
// "Microsoft\Windows NT\CurrentVersion" from the SOFTWARE registry hive at path
// `regHivePath`. This is used to detect the build version of the uvm.
func getOsBuildNumberFromRegistry(regHivePath string) (_ string, err error) {
var storeHandle, keyHandle winapi.ORHKey
var dataType, dataLen uint32
keyPath := "Microsoft\\Windows NT\\CurrentVersion"
valueName := "CurrentBuild"
dataLen = 16 // build version string can't be more than 5 wide chars?
dataBuf := make([]byte, dataLen)
if err = winapi.OROpenHive(regHivePath, &storeHandle); err != nil {
return "", fmt.Errorf("failed to open registry store at %s: %w", regHivePath, err)
}
defer func() {
if closeErr := winapi.ORCloseHive(storeHandle); closeErr != nil {
log.L.WithFields(logrus.Fields{
"error": closeErr,
"hive": regHivePath,
}).Warnf("failed to close hive")
}
}()
if err = winapi.OROpenKey(storeHandle, keyPath, &keyHandle); err != nil {
return "", fmt.Errorf("failed to open key at %s: %w", keyPath, err)
}
defer func() {
if closeErr := winapi.ORCloseKey(keyHandle); closeErr != nil {
log.L.WithFields(logrus.Fields{
"error": closeErr,
"hive": regHivePath,
"key": keyPath,
"value": valueName,
}).Warnf("failed to close hive key")
}
}()
if err = winapi.ORGetValue(keyHandle, "", valueName, &dataType, &dataBuf[0], &dataLen); err != nil {
return "", fmt.Errorf("failed to get value of %s: %w", valueName, err)
}
if dataType != uint32(winapi.REG_TYPE_SZ) {
return "", fmt.Errorf("unexpected build number data type (%d)", dataType)
}
return winapi.ParseUtf16LE(dataBuf[:(dataLen - 2)]), nil
}

View File

@@ -1,3 +1,5 @@
//go:build windows
package wclayer
import (

View File

@@ -11,7 +11,6 @@ import (
"github.com/Microsoft/hcsshim/internal/hcserror"
"github.com/Microsoft/hcsshim/internal/oc"
"github.com/Microsoft/hcsshim/osversion"
"go.opencensus.io/trace"
)
@@ -30,14 +29,17 @@ func ExpandScratchSize(ctx context.Context, path string, size uint64) (err error
return hcserror.New(err, title, "")
}
// Manually expand the volume now in order to work around bugs in 19H1 and
// prerelease versions of Vb. Remove once this is fixed in Windows.
if build := osversion.Build(); build >= osversion.V19H1 && build < 19020 {
err = expandSandboxVolume(ctx, path)
if err != nil {
return err
}
// Always expand the volume too. In case of legacy layers not expanding the volume here works because
// the PrepareLayer call internally handles the expansion. However, in other cases (like CimFS) we
// don't call PrepareLayer and so the volume will never be expanded. This also means in case of
// legacy layers, we might have a small perf hit because the VHD is mounted twice for expansion (once
// here and once during the PrepareLayer call). But as long as the perf hit is minimal, we should be
// okay.
err = expandSandboxVolume(ctx, path)
if err != nil {
return err
}
return nil
}

View File

@@ -154,7 +154,7 @@ func (r *legacyLayerReader) walkUntilCancelled() error {
}
return nil
})
if err == errorIterationCanceled {
if err == errorIterationCanceled { //nolint:errorlint // explicitly returned
return nil
}
if err == nil {
@@ -196,7 +196,7 @@ func findBackupStreamSize(r io.Reader) (int64, error) {
for {
hdr, err := br.Next()
if err != nil {
if err == io.EOF {
if errors.Is(err, io.EOF) {
err = nil
}
return 0, err
@@ -428,7 +428,7 @@ func (w *legacyLayerWriter) initUtilityVM() error {
// immutable.
err = cloneTree(w.parentRoots[0], w.destRoot, UtilityVMFilesPath, mutatedUtilityVMFiles)
if err != nil {
return fmt.Errorf("cloning the parent utility VM image failed: %s", err)
return fmt.Errorf("cloning the parent utility VM image failed: %w", err)
}
w.HasUtilityVM = true
}
@@ -451,7 +451,7 @@ func (w *legacyLayerWriter) reset() error {
for {
bhdr, err := br.Next()
if err == io.EOF {
if errors.Is(err, io.EOF) {
// end of backupstream data
break
}

View File

@@ -1,3 +1,5 @@
//go:build windows
package winapi
import (
@@ -34,7 +36,7 @@ type CimFsFileMetadata struct {
//sys CimDismountImage(volumeID *g) (hr error) = cimfs.CimDismountImage?
//sys CimCreateImage(imagePath string, oldFSName *uint16, newFSName *uint16, cimFSHandle *FsHandle) (hr error) = cimfs.CimCreateImage?
//sys CimCloseImage(cimFSHandle FsHandle) (hr error) = cimfs.CimCloseImage?
//sys CimCloseImage(cimFSHandle FsHandle) = cimfs.CimCloseImage?
//sys CimCommitImage(cimFSHandle FsHandle) (hr error) = cimfs.CimCommitImage?
//sys CimCreateFile(cimFSHandle FsHandle, path string, file *CimFsFileMetadata, cimStreamHandle *StreamHandle) (hr error) = cimfs.CimCreateFile?

View File

@@ -184,18 +184,12 @@ func _CMLocateDevNode(pdnDevInst *uint32, pDeviceID *uint16, uFlags uint32) (hr
return
}
func CimCloseImage(cimFSHandle FsHandle) (hr error) {
hr = procCimCloseImage.Find()
if hr != nil {
func CimCloseImage(cimFSHandle FsHandle) (err error) {
err = procCimCloseImage.Find()
if err != nil {
return
}
r0, _, _ := syscall.Syscall(procCimCloseImage.Addr(), 1, uintptr(cimFSHandle), 0, 0)
if int32(r0) < 0 {
if r0&0x1fff0000 == 0x00070000 {
r0 &= 0xffff
}
hr = syscall.Errno(r0)
}
syscall.Syscall(procCimCloseImage.Addr(), 1, uintptr(cimFSHandle), 0, 0)
return
}

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

View File

@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2015 Microsoft
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,4 +0,0 @@
package manifest
// This is so that tests can include the .syso to manifest them to pick up the right Windows build
// TODO: Auto-generation of the .syso through rsrc or similar.