github.com/containerd/containerd@v22.0.0-20200918172823-438c87b8e050+incompatible/snapshots/lcow/lcow.go (about)

     1  // +build windows
     2  
     3  /*
     4     Copyright The containerd Authors.
     5  
     6     Licensed under the Apache License, Version 2.0 (the "License");
     7     you may not use this file except in compliance with the License.
     8     You may obtain a copy of the License at
     9  
    10         http://www.apache.org/licenses/LICENSE-2.0
    11  
    12     Unless required by applicable law or agreed to in writing, software
    13     distributed under the License is distributed on an "AS IS" BASIS,
    14     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    15     See the License for the specific language governing permissions and
    16     limitations under the License.
    17  */
    18  
    19  package lcow
    20  
    21  import (
    22  	"context"
    23  	"encoding/json"
    24  	"fmt"
    25  	"io"
    26  	"os"
    27  	"path/filepath"
    28  	"strconv"
    29  	"strings"
    30  	"sync"
    31  	"time"
    32  
    33  	winfs "github.com/Microsoft/go-winio/pkg/fs"
    34  	"github.com/Microsoft/hcsshim/pkg/go-runhcs"
    35  	"github.com/containerd/containerd/errdefs"
    36  	"github.com/containerd/containerd/log"
    37  	"github.com/containerd/containerd/mount"
    38  	"github.com/containerd/containerd/plugin"
    39  	"github.com/containerd/containerd/snapshots"
    40  	"github.com/containerd/containerd/snapshots/storage"
    41  	"github.com/containerd/continuity/fs"
    42  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    43  	"github.com/pkg/errors"
    44  )
    45  
    46  func init() {
    47  	plugin.Register(&plugin.Registration{
    48  		Type: plugin.SnapshotPlugin,
    49  		ID:   "windows-lcow",
    50  		InitFn: func(ic *plugin.InitContext) (interface{}, error) {
    51  			ic.Meta.Platforms = append(ic.Meta.Platforms, ocispec.Platform{
    52  				OS:           "linux",
    53  				Architecture: "amd64",
    54  			})
    55  			return NewSnapshotter(ic.Root)
    56  		},
    57  	})
    58  }
    59  
    60  const (
    61  	rootfsSizeLabel = "containerd.io/snapshot/io.microsoft.container.storage.rootfs.size-gb"
    62  )
    63  
    64  type snapshotter struct {
    65  	root string
    66  	ms   *storage.MetaStore
    67  
    68  	scratchLock sync.Mutex
    69  }
    70  
    71  // NewSnapshotter returns a new windows snapshotter
    72  func NewSnapshotter(root string) (snapshots.Snapshotter, error) {
    73  	fsType, err := winfs.GetFileSystemType(root)
    74  	if err != nil {
    75  		return nil, err
    76  	}
    77  	if strings.ToLower(fsType) != "ntfs" {
    78  		return nil, errors.Wrapf(errdefs.ErrInvalidArgument, "%s is not on an NTFS volume - only NTFS volumes are supported", root)
    79  	}
    80  
    81  	if err := os.MkdirAll(root, 0700); err != nil {
    82  		return nil, err
    83  	}
    84  	ms, err := storage.NewMetaStore(filepath.Join(root, "metadata.db"))
    85  	if err != nil {
    86  		return nil, err
    87  	}
    88  
    89  	if err := os.Mkdir(filepath.Join(root, "snapshots"), 0700); err != nil && !os.IsExist(err) {
    90  		return nil, err
    91  	}
    92  
    93  	return &snapshotter{
    94  		root: root,
    95  		ms:   ms,
    96  	}, nil
    97  }
    98  
    99  // Stat returns the info for an active or committed snapshot by name or
   100  // key.
   101  //
   102  // Should be used for parent resolution, existence checks and to discern
   103  // the kind of snapshot.
   104  func (s *snapshotter) Stat(ctx context.Context, key string) (snapshots.Info, error) {
   105  	ctx, t, err := s.ms.TransactionContext(ctx, false)
   106  	if err != nil {
   107  		return snapshots.Info{}, err
   108  	}
   109  	defer t.Rollback()
   110  
   111  	_, info, _, err := storage.GetInfo(ctx, key)
   112  	return info, err
   113  }
   114  
   115  func (s *snapshotter) Update(ctx context.Context, info snapshots.Info, fieldpaths ...string) (snapshots.Info, error) {
   116  	ctx, t, err := s.ms.TransactionContext(ctx, true)
   117  	if err != nil {
   118  		return snapshots.Info{}, err
   119  	}
   120  	defer t.Rollback()
   121  
   122  	info, err = storage.UpdateInfo(ctx, info, fieldpaths...)
   123  	if err != nil {
   124  		return snapshots.Info{}, err
   125  	}
   126  
   127  	if err := t.Commit(); err != nil {
   128  		return snapshots.Info{}, err
   129  	}
   130  
   131  	return info, nil
   132  }
   133  
   134  func (s *snapshotter) Usage(ctx context.Context, key string) (snapshots.Usage, error) {
   135  	ctx, t, err := s.ms.TransactionContext(ctx, false)
   136  	if err != nil {
   137  		return snapshots.Usage{}, err
   138  	}
   139  	id, info, usage, err := storage.GetInfo(ctx, key)
   140  	t.Rollback() // transaction no longer needed at this point.
   141  
   142  	if err != nil {
   143  		return snapshots.Usage{}, err
   144  	}
   145  
   146  	if info.Kind == snapshots.KindActive {
   147  		du, err := fs.DiskUsage(ctx, s.getSnapshotDir(id))
   148  		if err != nil {
   149  			return snapshots.Usage{}, err
   150  		}
   151  		usage = snapshots.Usage(du)
   152  	}
   153  
   154  	return usage, nil
   155  }
   156  
   157  func (s *snapshotter) Prepare(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) {
   158  	return s.createSnapshot(ctx, snapshots.KindActive, key, parent, opts)
   159  }
   160  
   161  func (s *snapshotter) View(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) {
   162  	return s.createSnapshot(ctx, snapshots.KindView, key, parent, opts)
   163  }
   164  
   165  // Mounts returns the mounts for the transaction identified by key. Can be
   166  // called on an read-write or readonly transaction.
   167  //
   168  // This can be used to recover mounts after calling View or Prepare.
   169  func (s *snapshotter) Mounts(ctx context.Context, key string) ([]mount.Mount, error) {
   170  	ctx, t, err := s.ms.TransactionContext(ctx, false)
   171  	if err != nil {
   172  		return nil, err
   173  	}
   174  	defer t.Rollback()
   175  
   176  	snapshot, err := storage.GetSnapshot(ctx, key)
   177  	if err != nil {
   178  		return nil, errors.Wrap(err, "failed to get snapshot mount")
   179  	}
   180  	return s.mounts(snapshot), nil
   181  }
   182  
   183  func (s *snapshotter) Commit(ctx context.Context, name, key string, opts ...snapshots.Opt) error {
   184  	ctx, t, err := s.ms.TransactionContext(ctx, true)
   185  	if err != nil {
   186  		return err
   187  	}
   188  	defer func() {
   189  		if err != nil {
   190  			if rerr := t.Rollback(); rerr != nil {
   191  				log.G(ctx).WithError(rerr).Warn("failed to rollback transaction")
   192  			}
   193  		}
   194  	}()
   195  
   196  	// grab the existing id
   197  	id, _, _, err := storage.GetInfo(ctx, key)
   198  	if err != nil {
   199  		return err
   200  	}
   201  
   202  	usage, err := fs.DiskUsage(ctx, s.getSnapshotDir(id))
   203  	if err != nil {
   204  		return err
   205  	}
   206  
   207  	if _, err = storage.CommitActive(ctx, key, name, snapshots.Usage(usage), opts...); err != nil {
   208  		return errors.Wrap(err, "failed to commit snapshot")
   209  	}
   210  
   211  	return t.Commit()
   212  }
   213  
   214  // Remove abandons the transaction identified by key. All resources
   215  // associated with the key will be removed.
   216  func (s *snapshotter) Remove(ctx context.Context, key string) error {
   217  	ctx, t, err := s.ms.TransactionContext(ctx, true)
   218  	if err != nil {
   219  		return err
   220  	}
   221  	defer t.Rollback()
   222  
   223  	id, _, err := storage.Remove(ctx, key)
   224  	if err != nil {
   225  		return errors.Wrap(err, "failed to remove")
   226  	}
   227  
   228  	path := s.getSnapshotDir(id)
   229  	renamed := s.getSnapshotDir("rm-" + id)
   230  	if err := os.Rename(path, renamed); err != nil && !os.IsNotExist(err) {
   231  		return err
   232  	}
   233  
   234  	if err := t.Commit(); err != nil {
   235  		if err1 := os.Rename(renamed, path); err1 != nil {
   236  			// May cause inconsistent data on disk
   237  			log.G(ctx).WithError(err1).WithField("path", renamed).Errorf("Failed to rename after failed commit")
   238  		}
   239  		return errors.Wrap(err, "failed to commit")
   240  	}
   241  
   242  	if err := os.RemoveAll(renamed); err != nil {
   243  		// Must be cleaned up, any "rm-*" could be removed if no active transactions
   244  		log.G(ctx).WithError(err).WithField("path", renamed).Warnf("Failed to remove root filesystem")
   245  	}
   246  
   247  	return nil
   248  }
   249  
   250  // Walk the committed snapshots.
   251  func (s *snapshotter) Walk(ctx context.Context, fn snapshots.WalkFunc, fs ...string) error {
   252  	ctx, t, err := s.ms.TransactionContext(ctx, false)
   253  	if err != nil {
   254  		return err
   255  	}
   256  	defer t.Rollback()
   257  
   258  	return storage.WalkInfo(ctx, fn, fs...)
   259  }
   260  
   261  // Close closes the snapshotter
   262  func (s *snapshotter) Close() error {
   263  	return s.ms.Close()
   264  }
   265  
   266  func (s *snapshotter) mounts(sn storage.Snapshot) []mount.Mount {
   267  	var (
   268  		roFlag           string
   269  		source           string
   270  		parentLayerPaths []string
   271  	)
   272  
   273  	if sn.Kind == snapshots.KindView {
   274  		roFlag = "ro"
   275  	} else {
   276  		roFlag = "rw"
   277  	}
   278  
   279  	if len(sn.ParentIDs) == 0 || sn.Kind == snapshots.KindActive {
   280  		source = s.getSnapshotDir(sn.ID)
   281  		parentLayerPaths = s.parentIDsToParentPaths(sn.ParentIDs)
   282  	} else {
   283  		source = s.getSnapshotDir(sn.ParentIDs[0])
   284  		parentLayerPaths = s.parentIDsToParentPaths(sn.ParentIDs[1:])
   285  	}
   286  
   287  	// error is not checked here, as a string array will never fail to Marshal
   288  	parentLayersJSON, _ := json.Marshal(parentLayerPaths)
   289  	parentLayersOption := mount.ParentLayerPathsFlag + string(parentLayersJSON)
   290  
   291  	var mounts []mount.Mount
   292  	mounts = append(mounts, mount.Mount{
   293  		Source: source,
   294  		Type:   "lcow-layer",
   295  		Options: []string{
   296  			roFlag,
   297  			parentLayersOption,
   298  		},
   299  	})
   300  
   301  	return mounts
   302  }
   303  
   304  func (s *snapshotter) getSnapshotDir(id string) string {
   305  	return filepath.Join(s.root, "snapshots", id)
   306  }
   307  
   308  func (s *snapshotter) createSnapshot(ctx context.Context, kind snapshots.Kind, key, parent string, opts []snapshots.Opt) ([]mount.Mount, error) {
   309  	ctx, t, err := s.ms.TransactionContext(ctx, true)
   310  	if err != nil {
   311  		return nil, err
   312  	}
   313  	defer t.Rollback()
   314  
   315  	newSnapshot, err := storage.CreateSnapshot(ctx, kind, key, parent, opts...)
   316  	if err != nil {
   317  		return nil, errors.Wrap(err, "failed to create snapshot")
   318  	}
   319  
   320  	if kind == snapshots.KindActive {
   321  		log.G(ctx).Debug("createSnapshot active")
   322  		// Create the new snapshot dir
   323  		snDir := s.getSnapshotDir(newSnapshot.ID)
   324  		if err := os.MkdirAll(snDir, 0700); err != nil {
   325  			return nil, err
   326  		}
   327  
   328  		var snapshotInfo snapshots.Info
   329  		for _, o := range opts {
   330  			o(&snapshotInfo)
   331  		}
   332  
   333  		var sizeGB int
   334  		if sizeGBstr, ok := snapshotInfo.Labels[rootfsSizeLabel]; ok {
   335  			i32, err := strconv.ParseInt(sizeGBstr, 10, 32)
   336  			if err != nil {
   337  				return nil, errors.Wrapf(err, "failed to parse annotation %q=%q", rootfsSizeLabel, sizeGBstr)
   338  			}
   339  			sizeGB = int(i32)
   340  		}
   341  
   342  		scratchSource, err := s.openOrCreateScratch(ctx, sizeGB)
   343  		if err != nil {
   344  			return nil, err
   345  		}
   346  		defer scratchSource.Close()
   347  
   348  		// TODO: JTERRY75 - This has to be called sandbox.vhdx for the time
   349  		// being but it really is the scratch.vhdx Using this naming convention
   350  		// for now but this is not the kubernetes sandbox.
   351  		//
   352  		// Create the sandbox.vhdx for this snapshot from the cache.
   353  		destPath := filepath.Join(snDir, "sandbox.vhdx")
   354  		dest, err := os.OpenFile(destPath, os.O_RDWR|os.O_CREATE, 0700)
   355  		if err != nil {
   356  			return nil, errors.Wrap(err, "failed to create sandbox.vhdx in snapshot")
   357  		}
   358  		defer dest.Close()
   359  		if _, err := io.Copy(dest, scratchSource); err != nil {
   360  			dest.Close()
   361  			os.Remove(destPath)
   362  			return nil, errors.Wrap(err, "failed to copy cached scratch.vhdx to sandbox.vhdx in snapshot")
   363  		}
   364  	}
   365  
   366  	if err := t.Commit(); err != nil {
   367  		return nil, errors.Wrap(err, "commit failed")
   368  	}
   369  
   370  	return s.mounts(newSnapshot), nil
   371  }
   372  
   373  func (s *snapshotter) openOrCreateScratch(ctx context.Context, sizeGB int) (_ *os.File, err error) {
   374  	// Create the scratch.vhdx cache file if it doesn't already exit.
   375  	s.scratchLock.Lock()
   376  	defer s.scratchLock.Unlock()
   377  
   378  	vhdFileName := "scratch.vhdx"
   379  	if sizeGB > 0 {
   380  		vhdFileName = fmt.Sprintf("scratch_%d.vhdx", sizeGB)
   381  	}
   382  
   383  	scratchFinalPath := filepath.Join(s.root, vhdFileName)
   384  	scratchSource, err := os.OpenFile(scratchFinalPath, os.O_RDONLY, 0700)
   385  	if err != nil {
   386  		if !os.IsNotExist(err) {
   387  			return nil, errors.Wrapf(err, "failed to open vhd %s for read", vhdFileName)
   388  		}
   389  
   390  		log.G(ctx).Debugf("vhd %s not found, creating a new one", vhdFileName)
   391  
   392  		// Golang logic for ioutil.TempFile without the file creation
   393  		r := uint32(time.Now().UnixNano() + int64(os.Getpid()))
   394  		r = r*1664525 + 1013904223 // constants from Numerical Recipes
   395  
   396  		scratchTempName := fmt.Sprintf("scratch-%s-tmp.vhdx", strconv.Itoa(int(1e9 + r%1e9))[1:])
   397  		scratchTempPath := filepath.Join(s.root, scratchTempName)
   398  
   399  		// Create the scratch
   400  		rhcs := runhcs.Runhcs{
   401  			Debug:     true,
   402  			Log:       filepath.Join(s.root, "runhcs-scratch.log"),
   403  			LogFormat: runhcs.JSON,
   404  			Owner:     "containerd",
   405  		}
   406  
   407  		opt := runhcs.CreateScratchOpts{
   408  			SizeGB: sizeGB,
   409  		}
   410  
   411  		if err := rhcs.CreateScratchWithOpts(ctx, scratchTempPath, &opt); err != nil {
   412  			_ = os.Remove(scratchTempPath)
   413  			return nil, errors.Wrapf(err, "failed to create '%s' temp file", scratchTempName)
   414  		}
   415  		if err := os.Rename(scratchTempPath, scratchFinalPath); err != nil {
   416  			_ = os.Remove(scratchTempPath)
   417  			return nil, errors.Wrapf(err, "failed to rename '%s' temp file to 'scratch.vhdx'", scratchTempName)
   418  		}
   419  		scratchSource, err = os.OpenFile(scratchFinalPath, os.O_RDONLY, 0700)
   420  		if err != nil {
   421  			_ = os.Remove(scratchFinalPath)
   422  			return nil, errors.Wrap(err, "failed to open scratch.vhdx for read after creation")
   423  		}
   424  	}
   425  	return scratchSource, nil
   426  }
   427  
   428  func (s *snapshotter) parentIDsToParentPaths(parentIDs []string) []string {
   429  	var parentLayerPaths []string
   430  	for _, ID := range parentIDs {
   431  		parentLayerPaths = append(parentLayerPaths, s.getSnapshotDir(ID))
   432  	}
   433  	return parentLayerPaths
   434  }