diff --git a/cmd/containerd/builtins_windows.go b/cmd/containerd/builtins_windows.go index 381489170..95c9fb2f7 100644 --- a/cmd/containerd/builtins_windows.go +++ b/cmd/containerd/builtins_windows.go @@ -1,6 +1,7 @@ package main import ( + _ "github.com/containerd/containerd/diff/windows" _ "github.com/containerd/containerd/snapshots/windows" _ "github.com/containerd/containerd/windows" ) diff --git a/diff/windows/windows.go b/diff/windows/windows.go new file mode 100644 index 000000000..16bffca1e --- /dev/null +++ b/diff/windows/windows.go @@ -0,0 +1,169 @@ +// +build windows + +package windows + +import ( + "io" + "io/ioutil" + "strings" + "time" + + winio "github.com/Microsoft/go-winio" + "github.com/containerd/containerd/archive" + "github.com/containerd/containerd/archive/compression" + "github.com/containerd/containerd/content" + "github.com/containerd/containerd/diff" + "github.com/containerd/containerd/errdefs" + "github.com/containerd/containerd/images" + "github.com/containerd/containerd/log" + "github.com/containerd/containerd/metadata" + "github.com/containerd/containerd/mount" + "github.com/containerd/containerd/platforms" + "github.com/containerd/containerd/plugin" + digest "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "golang.org/x/net/context" +) + +func init() { + plugin.Register(&plugin.Registration{ + Type: plugin.DiffPlugin, + ID: "windows", + Requires: []plugin.Type{ + plugin.MetadataPlugin, + }, + InitFn: func(ic *plugin.InitContext) (interface{}, error) { + md, err := ic.Get(plugin.MetadataPlugin) + if err != nil { + return nil, err + } + + ic.Meta.Platforms = append(ic.Meta.Platforms, platforms.DefaultSpec()) + return NewWindowsDiff(md.(*metadata.DB).ContentStore()) + }, + }) +} + +type windowsDiff struct { + store content.Store +} + +var emptyDesc = ocispec.Descriptor{} + +// NewWindowsDiff is the Windows container layer implementation of diff.Differ. +func NewWindowsDiff(store content.Store) (diff.Differ, error) { + return &windowsDiff{ + store: store, + }, nil +} + +// Apply applies the content associated with the provided digests onto the +// provided mounts. Archive content will be extracted and decompressed if +// necessary. +func (s *windowsDiff) Apply(ctx context.Context, desc ocispec.Descriptor, mounts []mount.Mount) (d ocispec.Descriptor, err error) { + t1 := time.Now() + defer func() { + if err == nil { + log.G(ctx).WithFields(logrus.Fields{ + "d": time.Now().Sub(t1), + "dgst": desc.Digest, + "size": desc.Size, + "media": desc.MediaType, + }).Debugf("diff applied") + } + }() + var isCompressed bool + switch desc.MediaType { + case ocispec.MediaTypeImageLayer, images.MediaTypeDockerSchema2Layer: + case ocispec.MediaTypeImageLayerGzip, images.MediaTypeDockerSchema2LayerGzip: + isCompressed = true + default: + // Still apply all generic media types *.tar[.+]gzip and *.tar + if strings.HasSuffix(desc.MediaType, ".tar.gzip") || strings.HasSuffix(desc.MediaType, ".tar+gzip") { + isCompressed = true + } else if !strings.HasSuffix(desc.MediaType, ".tar") { + return emptyDesc, errors.Wrapf(errdefs.ErrNotImplemented, "unsupported diff media type: %v", desc.MediaType) + } + } + + ra, err := s.store.ReaderAt(ctx, desc.Digest) + if err != nil { + return emptyDesc, errors.Wrap(err, "failed to get reader from content store") + } + defer ra.Close() + + r := content.NewReader(ra) + if isCompressed { + ds, err := compression.DecompressStream(r) + if err != nil { + return emptyDesc, err + } + defer ds.Close() + r = ds + } + + digester := digest.Canonical.Digester() + rc := &readCounter{ + r: io.TeeReader(r, digester.Hash()), + } + + layer, parentLayerPaths, err := mountsToLayerAndParents(mounts) + if err != nil { + return emptyDesc, err + } + + // TODO darrenstahlmsft: When this is done isolated, we should disable these. + // it currently cannot be disabled, unless we add ref counting. Since this is + // temporary, leaving it enabled is OK for now. + if err := winio.EnableProcessPrivileges([]string{winio.SeBackupPrivilege, winio.SeRestorePrivilege}); err != nil { + return emptyDesc, err + } + + if _, err := archive.Apply(ctx, layer, rc, archive.WithParentLayers(parentLayerPaths), archive.AsWindowsContainerLayer()); err != nil { + return emptyDesc, err + } + + // Read any trailing data + if _, err := io.Copy(ioutil.Discard, rc); err != nil { + return emptyDesc, err + } + + return ocispec.Descriptor{ + MediaType: ocispec.MediaTypeImageLayer, + Size: rc.c, + Digest: digester.Digest(), + }, nil +} + +// DiffMounts creates a diff between the given mounts and uploads the result +// to the content store. +func (s *windowsDiff) DiffMounts(ctx context.Context, lower, upper []mount.Mount, opts ...diff.Opt) (d ocispec.Descriptor, err error) { + panic("not implemented on Windows") +} + +type readCounter struct { + r io.Reader + c int64 +} + +func (rc *readCounter) Read(p []byte) (n int, err error) { + n, err = rc.r.Read(p) + rc.c += int64(n) + return +} + +func mountsToLayerAndParents(mounts []mount.Mount) (string, []string, error) { + if len(mounts) != 1 { + return "", nil, errors.Wrap(errdefs.ErrInvalidArgument, "number of mounts should always be 1 for Windows layers") + } + layer := mounts[0].Source + + parentLayerPaths, err := mounts[0].GetParentPaths() + if err != nil { + return "", nil, err + } + + return layer, parentLayerPaths, nil +} diff --git a/mount/mount_windows.go b/mount/mount_windows.go index 8ad7eab12..df25ea2a8 100644 --- a/mount/mount_windows.go +++ b/mount/mount_windows.go @@ -1,6 +1,13 @@ package mount -import "github.com/pkg/errors" +import ( + "encoding/json" + "path/filepath" + "strings" + + "github.com/Microsoft/hcsshim" + "github.com/pkg/errors" +) var ( // ErrNotImplementOnWindows is returned when an action is not implemented for windows @@ -9,15 +16,73 @@ var ( // Mount to the provided target func (m *Mount) Mount(target string) error { - return ErrNotImplementOnWindows + home, layerID := filepath.Split(m.Source) + + parentLayerPaths, err := m.GetParentPaths() + if err != nil { + return err + } + + var di = hcsshim.DriverInfo{ + HomeDir: home, + } + + if err = hcsshim.ActivateLayer(di, layerID); err != nil { + return errors.Wrapf(err, "failed to activate layer %s", m.Source) + } + defer func() { + if err != nil { + hcsshim.DeactivateLayer(di, layerID) + } + }() + + if err = hcsshim.PrepareLayer(di, layerID, parentLayerPaths); err != nil { + return errors.Wrapf(err, "failed to prepare layer %s", m.Source) + } + defer func() { + if err != nil { + hcsshim.UnprepareLayer(di, layerID) + } + }() + return nil +} + +// ParentLayerPathsFlag is the options flag used to represent the JSON encoded +// list of parent layers required to use the layer +const ParentLayerPathsFlag = "parentLayerPaths=" + +// GetParentPaths of the mount +func (m *Mount) GetParentPaths() ([]string, error) { + var parentLayerPaths []string + for _, option := range m.Options { + if strings.HasPrefix(option, ParentLayerPathsFlag) { + err := json.Unmarshal([]byte(option[len(ParentLayerPathsFlag):]), &parentLayerPaths) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal parent layer paths from mount") + } + } + } + return parentLayerPaths, nil } // Unmount the mount at the provided path func Unmount(mount string, flags int) error { - return ErrNotImplementOnWindows + home, layerID := filepath.Split(mount) + var di = hcsshim.DriverInfo{ + HomeDir: home, + } + + if err := hcsshim.UnprepareLayer(di, layerID); err != nil { + return errors.Wrapf(err, "failed to unprepare layer %s", mount) + } + if err := hcsshim.DeactivateLayer(di, layerID); err != nil { + return errors.Wrapf(err, "failed to deactivate layer %s", mount) + } + + return nil } -// UnmountAll mounts at the provided path +// UnmountAll unmounts from the provided path func UnmountAll(mount string, flags int) error { - return ErrNotImplementOnWindows + return Unmount(mount, flags) } diff --git a/services/diff/service.go b/services/diff/service.go index f847693a4..4e1c9fcc6 100644 --- a/services/diff/service.go +++ b/services/diff/service.go @@ -30,9 +30,7 @@ func init() { Requires: []plugin.Type{ plugin.DiffPlugin, }, - Config: &config{ - Order: []string{"walking"}, - }, + Config: defaultDifferConfig, InitFn: func(ic *plugin.InitContext) (interface{}, error) { differs, err := ic.GetByType(plugin.DiffPlugin) if err != nil { diff --git a/services/diff/service_unix.go b/services/diff/service_unix.go new file mode 100644 index 000000000..39d88bbbb --- /dev/null +++ b/services/diff/service_unix.go @@ -0,0 +1,7 @@ +// +build !windows + +package diff + +var defaultDifferConfig = &config{ + Order: []string{"walking"}, +} diff --git a/services/diff/service_windows.go b/services/diff/service_windows.go new file mode 100644 index 000000000..afe193287 --- /dev/null +++ b/services/diff/service_windows.go @@ -0,0 +1,7 @@ +// +build windows + +package diff + +var defaultDifferConfig = &config{ + Order: []string{"windows"}, +} diff --git a/snapshots/windows/utilities.go b/snapshots/windows/utilities.go new file mode 100644 index 000000000..1854df66c --- /dev/null +++ b/snapshots/windows/utilities.go @@ -0,0 +1,16 @@ +// +build windows + +package windows + +import ( + "context" + + "github.com/containerd/containerd/log" + "github.com/containerd/containerd/snapshots/storage" +) + +func rollbackWithLogging(ctx context.Context, t storage.Transactor) { + if err := t.Rollback(); err != nil { + log.G(ctx).WithError(err).Warn("failed to rollback transaction") + } +} diff --git a/snapshots/windows/windows.go b/snapshots/windows/windows.go index 603428e17..be859baa2 100644 --- a/snapshots/windows/windows.go +++ b/snapshots/windows/windows.go @@ -4,16 +4,23 @@ package windows import ( "context" + "encoding/json" + "os" + "path/filepath" + "strings" + "syscall" + "unsafe" + "github.com/Microsoft/hcsshim" + "github.com/containerd/containerd/errdefs" + "github.com/containerd/containerd/fs" + "github.com/containerd/containerd/log" "github.com/containerd/containerd/mount" "github.com/containerd/containerd/plugin" "github.com/containerd/containerd/snapshots" + "github.com/containerd/containerd/snapshots/storage" "github.com/pkg/errors" -) - -var ( - // ErrNotImplemented is returned when an action is not implemented - ErrNotImplemented = errors.New("not implemented") + "golang.org/x/sys/windows" ) func init() { @@ -28,12 +35,38 @@ func init() { type snapshotter struct { root string + info hcsshim.DriverInfo + ms *storage.MetaStore } // NewSnapshotter returns a new windows snapshotter func NewSnapshotter(root string) (snapshots.Snapshotter, error) { + fsType, err := getFileSystemType(string(root[0])) + if err != nil { + return nil, err + } + if strings.ToLower(fsType) == "refs" { + return nil, errors.Wrapf(errdefs.ErrInvalidArgument, "%s is on an ReFS volume - ReFS volumes are not supported", root) + } + + if err := os.MkdirAll(root, 0700); err != nil { + return nil, err + } + ms, err := storage.NewMetaStore(filepath.Join(root, "metadata.db")) + if err != nil { + return nil, err + } + + if err := os.Mkdir(filepath.Join(root, "snapshots"), 0700); err != nil && !os.IsExist(err) { + return nil, err + } + return &snapshotter{ + info: hcsshim.DriverInfo{ + HomeDir: filepath.Join(root, "snapshots"), + }, root: root, + ms: ms, }, nil } @@ -42,50 +75,298 @@ func NewSnapshotter(root string) (snapshots.Snapshotter, error) { // // Should be used for parent resolution, existence checks and to discern // the kind of snapshot. -func (o *snapshotter) Stat(ctx context.Context, key string) (snapshots.Info, error) { - panic("not implemented") +func (s *snapshotter) Stat(ctx context.Context, key string) (snapshots.Info, error) { + ctx, t, err := s.ms.TransactionContext(ctx, false) + if err != nil { + return snapshots.Info{}, err + } + defer t.Rollback() + + _, info, _, err := storage.GetInfo(ctx, key) + if err != nil { + return snapshots.Info{}, err + } + + return info, nil } -func (o *snapshotter) Update(ctx context.Context, info snapshots.Info, fieldpaths ...string) (snapshots.Info, error) { - panic("not implemented") +func (s *snapshotter) Update(ctx context.Context, info snapshots.Info, fieldpaths ...string) (snapshots.Info, error) { + ctx, t, err := s.ms.TransactionContext(ctx, true) + if err != nil { + return snapshots.Info{}, err + } + + var committed bool + defer func() { + if committed == false { + rollbackWithLogging(ctx, t) + } + }() + + info, err = storage.UpdateInfo(ctx, info, fieldpaths...) + if err != nil { + return snapshots.Info{}, err + } + + if err := t.Commit(); err != nil { + return snapshots.Info{}, err + } + committed = true + + return info, nil } -func (o *snapshotter) Usage(ctx context.Context, key string) (snapshots.Usage, error) { - panic("not implemented") +func (s *snapshotter) Usage(ctx context.Context, key string) (snapshots.Usage, error) { + ctx, t, err := s.ms.TransactionContext(ctx, false) + if err != nil { + return snapshots.Usage{}, err + } + defer t.Rollback() + + _, info, usage, err := storage.GetInfo(ctx, key) + if err != nil { + return snapshots.Usage{}, err + } + + if info.Kind == snapshots.KindActive { + du := fs.Usage{ + Size: 0, + } + usage = snapshots.Usage(du) + } + + return usage, nil } -func (o *snapshotter) Prepare(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) { - panic("not implemented") +func (s *snapshotter) Prepare(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) { + return s.createSnapshot(ctx, snapshots.KindActive, key, parent, opts) } -func (o *snapshotter) View(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) { - panic("not implemented") +func (s *snapshotter) View(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) { + return s.createSnapshot(ctx, snapshots.KindView, key, parent, opts) } // Mounts returns the mounts for the transaction identified by key. Can be // called on an read-write or readonly transaction. // // This can be used to recover mounts after calling View or Prepare. -func (o *snapshotter) Mounts(ctx context.Context, key string) ([]mount.Mount, error) { - panic("not implemented") +func (s *snapshotter) Mounts(ctx context.Context, key string) ([]mount.Mount, error) { + ctx, t, err := s.ms.TransactionContext(ctx, false) + if err != nil { + return nil, err + } + defer t.Rollback() + snapshot, err := storage.GetSnapshot(ctx, key) + if err != nil { + return nil, errors.Wrap(err, "failed to get snapshot mount") + } + return s.mounts(snapshot), nil } -func (o *snapshotter) Commit(ctx context.Context, name, key string, opts ...snapshots.Opt) error { - panic("not implemented") +func (s *snapshotter) Commit(ctx context.Context, name, key string, opts ...snapshots.Opt) error { + ctx, t, err := s.ms.TransactionContext(ctx, true) + if err != nil { + return err + } + + var committed bool + defer func() { + if committed == false { + rollbackWithLogging(ctx, t) + } + }() + usage := fs.Usage{ + Size: 0, + } + + if _, err = storage.CommitActive(ctx, key, name, snapshots.Usage(usage), opts...); err != nil { + return errors.Wrap(err, "failed to commit snapshot") + } + + if err := t.Commit(); err != nil { + return err + } + committed = true + return nil } // Remove abandons the transaction identified by key. All resources // associated with the key will be removed. -func (o *snapshotter) Remove(ctx context.Context, key string) error { - panic("not implemented") +func (s *snapshotter) Remove(ctx context.Context, key string) error { + ctx, t, err := s.ms.TransactionContext(ctx, true) + if err != nil { + return err + } + + var committed bool + defer func() { + if committed == false { + rollbackWithLogging(ctx, t) + } + }() + + id, _, err := storage.Remove(ctx, key) + if err != nil { + return errors.Wrap(err, "failed to remove") + } + + path := s.getSnapshotDir(id) + renamedID := "rm-" + id + renamed := filepath.Join(s.root, "snapshots", "rm-"+id) + if err := os.Rename(path, renamed); err != nil && !os.IsNotExist(err) { + return err + } + + err = t.Commit() + if err != nil { + if err1 := os.Rename(renamed, path); err1 != nil { + // May cause inconsistent data on disk + log.G(ctx).WithError(err1).WithField("path", renamed).Errorf("Failed to rename after failed commit") + } + return errors.Wrap(err, "failed to commit") + } + committed = true + + if err := hcsshim.DestroyLayer(s.info, renamedID); err != nil { + // Must be cleaned up, any "rm-*" could be removed if no active transactions + log.G(ctx).WithError(err).WithField("path", renamed).Warnf("Failed to remove root filesystem") + } + + return nil } // Walk the committed snapshots. -func (o *snapshotter) Walk(ctx context.Context, fn func(context.Context, snapshots.Info) error) error { - panic("not implemented") +func (s *snapshotter) Walk(ctx context.Context, fn func(context.Context, snapshots.Info) error) error { + ctx, t, err := s.ms.TransactionContext(ctx, false) + if err != nil { + return err + } + defer t.Rollback() + return storage.WalkInfo(ctx, fn) } // Close closes the snapshotter -func (o *snapshotter) Close() error { - panic("not implemented") +func (s *snapshotter) Close() error { + return s.ms.Close() +} + +func (s *snapshotter) mounts(sn storage.Snapshot) []mount.Mount { + var ( + roFlag string + ) + + if sn.Kind == snapshots.KindView { + roFlag = "ro" + } else { + roFlag = "rw" + } + + parentLayerPaths := s.parentIDsToParentPaths(sn.ParentIDs) + // error is not checked here, as a string array will never fail to Marshal + parentLayersJSON, _ := json.Marshal(parentLayerPaths) + parentLayersOption := mount.ParentLayerPathsFlag + string(parentLayersJSON) + + var mounts []mount.Mount + mounts = append(mounts, mount.Mount{ + Source: s.getSnapshotDir(sn.ID), + Type: "windows-layer", + Options: []string{ + roFlag, + parentLayersOption, + }, + }) + + return mounts +} + +func (s *snapshotter) getSnapshotDir(id string) string { + return filepath.Join(s.root, "snapshots", id) +} + +func (s *snapshotter) createSnapshot(ctx context.Context, kind snapshots.Kind, key, parent string, opts []snapshots.Opt) ([]mount.Mount, error) { + ctx, t, err := s.ms.TransactionContext(ctx, true) + if err != nil { + return nil, err + } + + var committed bool + defer func() { + if committed == false { + rollbackWithLogging(ctx, t) + } + }() + + newSnapshot, err := storage.CreateSnapshot(ctx, kind, key, parent, opts...) + if err != nil { + return nil, errors.Wrap(err, "failed to create snapshot") + } + + switch kind { + case snapshots.KindView: + var parentID string + if len(newSnapshot.ParentIDs) != 0 { + parentID = newSnapshot.ParentIDs[0] + } + if err := hcsshim.CreateLayer(s.info, newSnapshot.ID, parentID); err != nil { + return nil, errors.Wrap(err, "failed to create layer") + } + case snapshots.KindActive: + parentLayerPaths := s.parentIDsToParentPaths(newSnapshot.ParentIDs) + + var parentPath string + if len(parentLayerPaths) != 0 { + parentPath = parentLayerPaths[0] + } + + if err := hcsshim.CreateSandboxLayer(s.info, newSnapshot.ID, parentPath, parentLayerPaths); err != nil { + return nil, errors.Wrap(err, "failed to create sandbox layer") + } + + // TODO(darrenstahlmsft): Allow changing sandbox size + } + + if err := t.Commit(); err != nil { + return nil, errors.Wrap(err, "commit failed") + } + committed = true + + return s.mounts(newSnapshot), nil +} + +func (s *snapshotter) parentIDsToParentPaths(parentIDs []string) []string { + var parentLayerPaths []string + for _, ID := range parentIDs { + parentLayerPaths = append(parentLayerPaths, s.getSnapshotDir(ID)) + } + return parentLayerPaths +} + +// getFileSystemType obtains the type of a file system through GetVolumeInformation +// https://msdn.microsoft.com/en-us/library/windows/desktop/aa364993(v=vs.85).aspx +func getFileSystemType(drive string) (fsType string, hr error) { + var ( + modkernel32 = windows.NewLazySystemDLL("kernel32.dll") + procGetVolumeInformation = modkernel32.NewProc("GetVolumeInformationW") + buf = make([]uint16, 255) + size = windows.MAX_PATH + 1 + ) + if len(drive) != 1 { + return "", errors.New("getFileSystemType must be called with a drive letter") + } + drive += `:\` + n := uintptr(unsafe.Pointer(nil)) + r0, _, _ := syscall.Syscall9(procGetVolumeInformation.Addr(), 8, uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(drive))), n, n, n, n, n, uintptr(unsafe.Pointer(&buf[0])), uintptr(size), 0) + if int32(r0) < 0 { + hr = syscall.Errno(win32FromHresult(r0)) + } + fsType = windows.UTF16ToString(buf) + return +} + +// win32FromHresult is a helper function to get the win32 error code from an HRESULT +func win32FromHresult(hr uintptr) uintptr { + if hr&0x1fff0000 == 0x00070000 { + return hr & 0xffff + } + return hr }