mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
Add checkpoint/restore support for tmpfs with file backend.
Tmpfs with file-backed are widely used: 1. Via --overlay2 flag. The default is root:self so the root mount uses this. 2. EmptyDir mounts with default medium are created as tmpfs with file backend. This change unblocks (1) use case from being used with checkpoint/restore. For (2), checkpoint/restore is not yet supported in multicontainers. Most notably, this allows checkpoint/restore to work with default runsc flags. PiperOrigin-RevId: 586291915
This commit is contained in:
@@ -290,7 +290,6 @@ docker-tests: load-basic $(RUNTIME_BIN)
|
||||
@$(call install_runtime,$(RUNTIME)-dcache,--fdlimit=2000 --dcache=100) # Used by TestDentryCacheLimit.
|
||||
@$(call install_runtime,$(RUNTIME)-host-uds,--host-uds=all) # Used by TestHostSocketConnect.
|
||||
@$(call install_runtime,$(RUNTIME)-overlay,--overlay2=all:self) # Used by TestOverlay*.
|
||||
@$(call install_runtime,$(RUNTIME)-no-overlay,--overlay2=none) # Used by TestCheckpointRestore.
|
||||
@$(call test_runtime,$(RUNTIME),$(INTEGRATION_TARGETS) //test/e2e:integration_runtime_test)
|
||||
.PHONY: docker-tests
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
// Compile-time assertion that filesystem implements vfs.FilesystemImplSaveRestoreExtension.
|
||||
var _ = vfs.FilesystemImplSaveRestoreExtension((*filesystem)(nil))
|
||||
|
||||
// PreprareSave implements vfs.FilesystemImplSaveRestoreExtension.PrepareSave.
|
||||
// PrepareSave implements vfs.FilesystemImplSaveRestoreExtension.PrepareSave.
|
||||
func (fs *filesystem) PrepareSave(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ type savedDentryRW struct {
|
||||
write bool
|
||||
}
|
||||
|
||||
// PreprareSave implements vfs.FilesystemImplSaveRestoreExtension.PrepareSave.
|
||||
// PrepareSave implements vfs.FilesystemImplSaveRestoreExtension.PrepareSave.
|
||||
func (fs *filesystem) PrepareSave(ctx context.Context) error {
|
||||
if len(fs.iopts.UniqueID) == 0 {
|
||||
return fmt.Errorf("gofer.filesystem with no UniqueID cannot be saved")
|
||||
|
||||
@@ -14,13 +14,19 @@
|
||||
|
||||
package tmpfs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gvisor.dev/gvisor/pkg/context"
|
||||
"gvisor.dev/gvisor/pkg/sentry/pgalloc"
|
||||
"gvisor.dev/gvisor/pkg/sentry/vfs"
|
||||
)
|
||||
|
||||
// afterLoad is called by stateify.
|
||||
func (fs *filesystem) afterLoad() {
|
||||
if fs.privateMF {
|
||||
// TODO(b/271612187): Add S/R support.
|
||||
panic("S/R not supported for private memory files")
|
||||
if !fs.privateMF {
|
||||
fs.mf = fs.mfp.MemoryFile()
|
||||
}
|
||||
fs.mf = fs.mfp.MemoryFile()
|
||||
}
|
||||
|
||||
// saveParent is called by stateify.
|
||||
@@ -28,7 +34,40 @@ func (d *dentry) saveParent() *dentry {
|
||||
return d.parent.Load()
|
||||
}
|
||||
|
||||
// saveParent is called by stateify.
|
||||
// loadParent is called by stateify.
|
||||
func (d *dentry) loadParent(parent *dentry) {
|
||||
d.parent.Store(parent)
|
||||
}
|
||||
|
||||
// PrepareSave implements vfs.FilesystemImplSaveRestoreExtension.PrepareSave.
|
||||
func (fs *filesystem) PrepareSave(ctx context.Context) error {
|
||||
if !fs.privateMF {
|
||||
return nil
|
||||
}
|
||||
mfmapv := ctx.Value(vfs.CtxFilesystemMemoryFileMap)
|
||||
if mfmapv == nil {
|
||||
return fmt.Errorf("CtxFilesystemMemoryFileMap was not provided")
|
||||
}
|
||||
mfmap := mfmapv.(map[string]*pgalloc.MemoryFile)
|
||||
mfmap[fs.uniqueID] = fs.mf
|
||||
return nil
|
||||
}
|
||||
|
||||
// CompleteRestore implements
|
||||
// vfs.FilesystemImplSaveRestoreExtension.CompleteRestore.
|
||||
func (fs *filesystem) CompleteRestore(ctx context.Context, opts vfs.CompleteRestoreOptions) error {
|
||||
if !fs.privateMF {
|
||||
return nil
|
||||
}
|
||||
mfmapv := ctx.Value(vfs.CtxFilesystemMemoryFileMap)
|
||||
if mfmapv == nil {
|
||||
return fmt.Errorf("CtxFilesystemMemoryFileMap was not provided")
|
||||
}
|
||||
mfmap := mfmapv.(map[string]*pgalloc.MemoryFile)
|
||||
mf, ok := mfmap[fs.uniqueID]
|
||||
if !ok {
|
||||
return fmt.Errorf("memory file for %q not found in CtxFilesystemMemoryFileMap", fs.uniqueID)
|
||||
}
|
||||
fs.mf = mf
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ import (
|
||||
"gvisor.dev/gvisor/pkg/atomicbitops"
|
||||
"gvisor.dev/gvisor/pkg/context"
|
||||
"gvisor.dev/gvisor/pkg/errors/linuxerr"
|
||||
"gvisor.dev/gvisor/pkg/fd"
|
||||
"gvisor.dev/gvisor/pkg/hostarch"
|
||||
"gvisor.dev/gvisor/pkg/sentry/kernel/auth"
|
||||
"gvisor.dev/gvisor/pkg/sentry/kernel/time"
|
||||
@@ -71,6 +70,10 @@ type filesystem struct {
|
||||
// tmpfs takes ownership of mf. privateMF is immutable.
|
||||
privateMF bool
|
||||
|
||||
// uniqueID is an opaque string used to reassociate the filesystem with its
|
||||
// private MemoryFile during checkpoint and restore.
|
||||
uniqueID string
|
||||
|
||||
// mfp is used to provide mf, when privateMF == false. This is required to
|
||||
// re-provide mf on restore. mfp is immutable.
|
||||
mfp pgalloc.MemoryFileProvider
|
||||
@@ -142,9 +145,9 @@ type FilesystemOpts struct {
|
||||
// MaxFilenameLen is the maximum filename length allowed by the tmpfs.
|
||||
MaxFilenameLen int
|
||||
|
||||
// FilestoreFD is the FD for the memory file that will be used to store file
|
||||
// data. If this is nil, then MemoryFileProviderFromContext() is used.
|
||||
FilestoreFD *fd.FD
|
||||
// MemoryFile is the memory file that will be used to store file data. If
|
||||
// this is nil, then MemoryFileProviderFromContext() is used.
|
||||
MemoryFile *pgalloc.MemoryFile
|
||||
|
||||
// DisableDefaultSizeLimit disables setting a default size limit. In Linux,
|
||||
// SB_KERNMOUNT has this effect on tmpfs mounts; see mm/shmem.c:shmem_fill_super().
|
||||
@@ -153,6 +156,10 @@ type FilesystemOpts struct {
|
||||
// AllowXattrPrefix is a set of xattr namespace prefixes that this
|
||||
// tmpfs mount will allow.
|
||||
AllowXattrPrefix []string
|
||||
|
||||
// If UniqueID is non-empty, it is an opaque string used to reassociate the
|
||||
// filesystem with its private MemoryFile during checkpoint and restore.
|
||||
UniqueID string
|
||||
}
|
||||
|
||||
// Default size limit mount option. It is immutable after initialization.
|
||||
@@ -185,7 +192,7 @@ func (fstype FilesystemType) GetFilesystem(ctx context.Context, vfsObj *vfs.Virt
|
||||
}
|
||||
mf := mfp.MemoryFile()
|
||||
privateMF := false
|
||||
|
||||
uniqueID := ""
|
||||
rootFileType := uint16(linux.S_IFDIR)
|
||||
disableDefaultSizeLimit := false
|
||||
newFSType := vfs.FilesystemType(&fstype)
|
||||
@@ -209,32 +216,19 @@ func (fstype FilesystemType) GetFilesystem(ctx context.Context, vfsObj *vfs.Virt
|
||||
newFSType = tmpfsOpts.FilesystemType
|
||||
}
|
||||
disableDefaultSizeLimit = tmpfsOpts.DisableDefaultSizeLimit
|
||||
if tmpfsOpts.FilestoreFD != nil {
|
||||
mfOpts := pgalloc.MemoryFileOpts{
|
||||
// tmpfsOpts.FilestoreFD may be backed by a file on disk (not memfd),
|
||||
// which needs to be decommited on destroy to release disk space.
|
||||
DecommitOnDestroy: true,
|
||||
// sentry's seccomp filters don't allow the mmap(2) syscalls that
|
||||
// pgalloc.IMAWorkAroundForMemFile() uses. Users of tmpfsOpts.FilestoreFD
|
||||
// are expected to have performed the work around outside the sandbox.
|
||||
DisableIMAWorkAround: true,
|
||||
// Custom filestore FDs are usually backed by files on disk. Ideally we
|
||||
// would confirm with fstatfs(2) but that is prohibited by seccomp.
|
||||
DiskBackedFile: true,
|
||||
}
|
||||
var err error
|
||||
mf, err = pgalloc.NewMemoryFile(tmpfsOpts.FilestoreFD.ReleaseToFile("overlay-filestore"), mfOpts)
|
||||
if err != nil {
|
||||
ctx.Warningf("tmpfs.FilesystemType.GetFilesystem: pgalloc.NewMemoryFile failed: %v", err)
|
||||
return nil, nil, err
|
||||
}
|
||||
if tmpfsOpts.MemoryFile != nil {
|
||||
mf = tmpfsOpts.MemoryFile
|
||||
privateMF = true
|
||||
}
|
||||
|
||||
uniqueID = tmpfsOpts.UniqueID
|
||||
for _, xattr := range tmpfsOpts.AllowXattrPrefix {
|
||||
allowXattrPrefix[xattr] = struct{}{}
|
||||
}
|
||||
}
|
||||
if privateMF && uniqueID == "" {
|
||||
ctx.Warningf("tmpfs.FilesystemType.GetFilesystem: privateMF requires uniqueID to be set")
|
||||
return nil, nil, linuxerr.EINVAL
|
||||
}
|
||||
|
||||
mopts := vfs.GenericParseMountOptions(opts.Data)
|
||||
rootMode := linux.FileMode(0777)
|
||||
@@ -318,6 +312,7 @@ func (fstype FilesystemType) GetFilesystem(ctx context.Context, vfsObj *vfs.Virt
|
||||
fs := filesystem{
|
||||
mf: mf,
|
||||
privateMF: privateMF,
|
||||
uniqueID: uniqueID,
|
||||
mfp: mfp,
|
||||
clock: clock,
|
||||
devMinor: devMinor,
|
||||
|
||||
@@ -332,6 +332,10 @@ type Kernel struct {
|
||||
// devGofers maps container ID to its device gofer client.
|
||||
devGofers map[string]*devutil.GoferClient `state:"nosave"`
|
||||
devGofersMu sync.Mutex `state:"nosave"`
|
||||
|
||||
// savedMFOwners is the list of filesystem unique IDs (in order) for
|
||||
// filesystems that have saved their MemoryFile in the state file.
|
||||
savedMFOwners []string
|
||||
}
|
||||
|
||||
// InitKernelArgs holds arguments to Init.
|
||||
@@ -526,12 +530,21 @@ func (k *Kernel) SaveTo(ctx context.Context, w wire.Writer) error {
|
||||
return fmt.Errorf("failed to invalidate unsavable mappings: %v", err)
|
||||
}
|
||||
|
||||
// Capture all private memory files.
|
||||
mfsToSave := make(map[string]*pgalloc.MemoryFile)
|
||||
vfsCtx := context.WithValue(ctx, vfs.CtxFilesystemMemoryFileMap, mfsToSave)
|
||||
// Prepare filesystems for saving. This must be done after
|
||||
// invalidateUnsavableMappings(), since dropping memory mappings may
|
||||
// affect filesystem state (e.g. page cache reference counts).
|
||||
if err := k.vfs.PrepareSave(ctx); err != nil {
|
||||
if err := k.vfs.PrepareSave(vfsCtx); err != nil {
|
||||
return err
|
||||
}
|
||||
// Generate the order in which memory files are saved in the state file.
|
||||
// Note that this must happen before saving the kernel state.
|
||||
k.savedMFOwners = make([]string, 0, len(mfsToSave))
|
||||
for fsID := range mfsToSave {
|
||||
k.savedMFOwners = append(k.savedMFOwners, fsID)
|
||||
}
|
||||
|
||||
// Save the CPUID FeatureSet before the rest of the kernel so we can
|
||||
// verify its compatibility on restore before attempting to restore the
|
||||
@@ -564,12 +577,17 @@ func (k *Kernel) SaveTo(ctx context.Context, w wire.Writer) error {
|
||||
log.Infof("Kernel save stats: %s", stats.String())
|
||||
log.Infof("Kernel save took [%s].", time.Since(kernelStart))
|
||||
|
||||
// Save the memory file's state.
|
||||
// Save the memory files' state.
|
||||
memoryStart := time.Now()
|
||||
if err := k.mf.SaveTo(ctx, w); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("Memory save took [%s].", time.Since(memoryStart))
|
||||
for _, fsID := range k.savedMFOwners {
|
||||
if err := mfsToSave[fsID].SaveTo(ctx, w); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
log.Infof("Memory files save took [%s].", time.Since(memoryStart))
|
||||
|
||||
log.Infof("Overall save took [%s].", time.Since(saveStart))
|
||||
|
||||
@@ -642,12 +660,28 @@ func (k *Kernel) LoadFrom(ctx context.Context, r wire.Reader, timeReady chan str
|
||||
// Restore the root network stack.
|
||||
k.rootNetworkNamespace.RestoreRootStack(net)
|
||||
|
||||
// Load the memory file's state.
|
||||
// Load the memory files' state.
|
||||
memoryStart := time.Now()
|
||||
if err := k.mf.LoadFrom(ctx, r); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("Memory load took [%s].", time.Since(memoryStart))
|
||||
var mfmap map[string]*pgalloc.MemoryFile
|
||||
if mfmapv := ctx.Value(vfs.CtxFilesystemMemoryFileMap); mfmapv != nil {
|
||||
mfmap = mfmapv.(map[string]*pgalloc.MemoryFile)
|
||||
}
|
||||
if len(mfmap) != len(k.savedMFOwners) {
|
||||
return fmt.Errorf("inconsistent private memory files on restore: savedMFOwners = %v, CtxFilesystemMemoryFileMap = %v", k.savedMFOwners, mfmap)
|
||||
}
|
||||
for _, fsID := range k.savedMFOwners {
|
||||
mf, ok := mfmap[fsID]
|
||||
if !ok {
|
||||
return fmt.Errorf("saved memory file for %q was not configured on restore", fsID)
|
||||
}
|
||||
if err := mf.LoadFrom(ctx, r); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
log.Infof("Memory files load took [%s].", time.Since(memoryStart))
|
||||
|
||||
log.Infof("Overall load took [%s]", time.Since(loadStart))
|
||||
|
||||
|
||||
@@ -32,6 +32,10 @@ const (
|
||||
// mapping filesystem unique IDs (cf. gofer.InternalFilesystemOptions.UniqueID)
|
||||
// to host FDs.
|
||||
CtxRestoreFilesystemFDMap
|
||||
|
||||
// CtxFilesystemMemoryFileMap is a Context.Value key for mapping tmpfs unique
|
||||
// IDs to private memory files. This is used for save/restore.
|
||||
CtxFilesystemMemoryFileMap
|
||||
)
|
||||
|
||||
// MountNamespaceFromContext returns the MountNamespace used by ctx. If ctx is
|
||||
|
||||
@@ -468,20 +468,6 @@ func WaitForHTTP(ip string, port int, timeout time.Duration) error {
|
||||
return Poll(cb, timeout)
|
||||
}
|
||||
|
||||
// HTTPRequestSucceeds sends a request to a given url and checks that the status is OK.
|
||||
func HTTPRequestSucceeds(client http.Client, server string, port int) error {
|
||||
url := fmt.Sprintf("http://%s:%d", server, port)
|
||||
// Ensure that content is being served.
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reaching http server: %v", err)
|
||||
}
|
||||
if want := http.StatusOK; resp.StatusCode != want {
|
||||
return fmt.Errorf("wrong response code, got: %d, want: %d", resp.StatusCode, want)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reaper reaps child processes.
|
||||
type Reaper struct {
|
||||
// mu protects ch, which will be nil if the reaper is not running.
|
||||
|
||||
+53
-6
@@ -55,6 +55,7 @@ import (
|
||||
"gvisor.dev/gvisor/pkg/sentry/inet"
|
||||
"gvisor.dev/gvisor/pkg/sentry/kernel"
|
||||
"gvisor.dev/gvisor/pkg/sentry/kernel/auth"
|
||||
"gvisor.dev/gvisor/pkg/sentry/pgalloc"
|
||||
"gvisor.dev/gvisor/pkg/sentry/vfs"
|
||||
"gvisor.dev/gvisor/runsc/config"
|
||||
"gvisor.dev/gvisor/runsc/specutils"
|
||||
@@ -521,7 +522,7 @@ func (c *containerMounter) createMountNamespace(ctx context.Context, conf *confi
|
||||
if rootfsConf.IsFilestorePresent() {
|
||||
filestoreFD = c.goferFilestoreFDs.removeAsFD()
|
||||
}
|
||||
opts, cleanup, err = c.configureOverlay(ctx, conf, creds, opts, fsName, filestoreFD, rootfsConf)
|
||||
opts, cleanup, err = c.configureOverlay(ctx, conf, creds, opts, fsName, filestoreFD, rootfsConf, "/")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("mounting root with overlay: %w", err)
|
||||
}
|
||||
@@ -562,7 +563,7 @@ func (c *containerMounter) createMountNamespace(ctx context.Context, conf *confi
|
||||
// layer using tmpfs, and return overlay mount options. "cleanup" must be called
|
||||
// after the options have been used to mount the overlay, to release refs on
|
||||
// lower and upper mounts.
|
||||
func (c *containerMounter) configureOverlay(ctx context.Context, conf *config.Config, creds *auth.Credentials, lowerOpts *vfs.MountOptions, lowerFSName string, filestoreFD *fd.FD, mountConf GoferMountConf) (*vfs.MountOptions, func(), error) {
|
||||
func (c *containerMounter) configureOverlay(ctx context.Context, conf *config.Config, creds *auth.Credentials, lowerOpts *vfs.MountOptions, lowerFSName string, filestoreFD *fd.FD, mountConf GoferMountConf, dst string) (*vfs.MountOptions, func(), error) {
|
||||
// First copy options from lower layer to upper layer and overlay. Clear
|
||||
// filesystem specific options.
|
||||
upperOpts := *lowerOpts
|
||||
@@ -602,11 +603,19 @@ func (c *containerMounter) configureOverlay(ctx context.Context, conf *config.Co
|
||||
// Upper is a tmpfs mount to keep all modifications inside the sandbox.
|
||||
tmpfsOpts := tmpfs.FilesystemOpts{
|
||||
RootFileType: uint16(rootType),
|
||||
FilestoreFD: filestoreFD,
|
||||
// If a mount is being overlaid, it should not be limited by the default
|
||||
// tmpfs size limit.
|
||||
DisableDefaultSizeLimit: true,
|
||||
}
|
||||
if filestoreFD != nil {
|
||||
// Create memory file for disk-backed overlays.
|
||||
mf, err := createPrivateMemoryFile(filestoreFD.ReleaseToFile("overlay-filestore"))
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to create memory file for overlay: %v", err)
|
||||
}
|
||||
tmpfsOpts.MemoryFile = mf
|
||||
tmpfsOpts.UniqueID = dst
|
||||
}
|
||||
upperOpts.GetFilesystemOptions.InternalData = tmpfsOpts
|
||||
upper, err := c.k.VFS().MountDisconnected(ctx, creds, "" /* source */, tmpfs.Name, &upperOpts)
|
||||
if err != nil {
|
||||
@@ -800,7 +809,7 @@ func (c *containerMounter) mountSubmount(ctx context.Context, spec *specs.Spec,
|
||||
if submount.goferMountConf.ShouldUseOverlayfs() {
|
||||
log.Infof("Adding overlay on top of mount %q", submount.mount.Destination)
|
||||
var cleanup func()
|
||||
opts, cleanup, err = c.configureOverlay(ctx, conf, creds, opts, fsName, submount.filestoreFD, submount.goferMountConf)
|
||||
opts, cleanup, err = c.configureOverlay(ctx, conf, creds, opts, fsName, submount.filestoreFD, submount.goferMountConf, submount.mount.Destination)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("mounting volume with overlay at %q: %w", submount.mount.Destination, err)
|
||||
}
|
||||
@@ -855,8 +864,13 @@ func getMountNameAndOptions(spec *specs.Spec, conf *config.Config, m *mountInfo,
|
||||
return "", nil, err
|
||||
}
|
||||
if m.filestoreFD != nil {
|
||||
mf, err := createPrivateMemoryFile(m.filestoreFD.ReleaseToFile("tmpfs-filestore"))
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("failed to create memory file for tmpfs: %v", err)
|
||||
}
|
||||
internalData = tmpfs.FilesystemOpts{
|
||||
FilestoreFD: m.filestoreFD,
|
||||
MemoryFile: mf,
|
||||
UniqueID: m.mount.Destination,
|
||||
// If a mount is being overlaid with tmpfs, it should not be limited by
|
||||
// the default tmpfs size limit.
|
||||
DisableDefaultSizeLimit: true,
|
||||
@@ -936,6 +950,21 @@ func parseKeyValue(s string) (string, string, bool) {
|
||||
return strings.TrimSpace(tokens[0]), strings.TrimSpace(tokens[1]), true
|
||||
}
|
||||
|
||||
func createPrivateMemoryFile(file *os.File) (*pgalloc.MemoryFile, error) {
|
||||
mfOpts := pgalloc.MemoryFileOpts{
|
||||
// Private memory files are usually backed by files on disk. Ideally we
|
||||
// would confirm with fstatfs(2) but that is prohibited by seccomp.
|
||||
DiskBackedFile: true,
|
||||
// Disk backed files need to be decommited on destroy to release disk space.
|
||||
DecommitOnDestroy: true,
|
||||
// sentry's seccomp filters don't allow the mmap(2) syscalls that
|
||||
// pgalloc.IMAWorkAroundForMemFile() uses. Users of private memory files
|
||||
// are expected to have performed the work around outside the sandbox.
|
||||
DisableIMAWorkAround: true,
|
||||
}
|
||||
return pgalloc.NewMemoryFile(file, mfOpts)
|
||||
}
|
||||
|
||||
// mountTmp mounts an internal tmpfs at '/tmp' if it's safe to do so.
|
||||
// Technically we don't have to mount tmpfs at /tmp, as we could just rely on
|
||||
// the host /tmp, but this is a nice optimization, and fixes some apps that call
|
||||
@@ -1101,8 +1130,19 @@ func (c *containerMounter) makeMountPoint(ctx context.Context, creds *auth.Crede
|
||||
// configureRestore returns an updated context.Context including filesystem
|
||||
// state used by restore defined by conf.
|
||||
func (c *containerMounter) configureRestore(ctx context.Context) (context.Context, error) {
|
||||
// Compare createMountNamespace(); rootfs always consumes a gofer FD and a
|
||||
// filestore FD is consumed if the rootfs GoferMountConf indicates so.
|
||||
fdmap := make(map[string]int)
|
||||
fdmap["/"] = c.goferFDs.remove()
|
||||
mfmap := make(map[string]*pgalloc.MemoryFile)
|
||||
if rootfsConf := c.goferMountConfs[0]; rootfsConf.IsFilestorePresent() {
|
||||
mf, err := createPrivateMemoryFile(c.goferFilestoreFDs.removeAsFD().ReleaseToFile("overlay-filestore"))
|
||||
if err != nil {
|
||||
return ctx, fmt.Errorf("failed to create private memory file for mount rootfs: %w", err)
|
||||
}
|
||||
mfmap["/"] = mf
|
||||
}
|
||||
// prepareMounts() consumes the remaining FDs for submounts.
|
||||
mounts, err := c.prepareMounts()
|
||||
if err != nil {
|
||||
return ctx, err
|
||||
@@ -1112,8 +1152,15 @@ func (c *containerMounter) configureRestore(ctx context.Context) (context.Contex
|
||||
if submount.goferFD != nil {
|
||||
fdmap[submount.mount.Destination] = submount.goferFD.Release()
|
||||
}
|
||||
if submount.filestoreFD != nil {
|
||||
mf, err := createPrivateMemoryFile(submount.filestoreFD.ReleaseToFile("overlay-filestore"))
|
||||
if err != nil {
|
||||
return ctx, fmt.Errorf("failed to create private memory file for mount %q: %w", submount.mount.Destination, err)
|
||||
}
|
||||
mfmap[submount.mount.Destination] = mf
|
||||
}
|
||||
}
|
||||
return context.WithValue(ctx, vfs.CtxRestoreFilesystemFDMap, fdmap), nil
|
||||
return context.WithValue(context.WithValue(ctx, vfs.CtxRestoreFilesystemFDMap, fdmap), vfs.CtxFilesystemMemoryFileMap, mfmap), nil
|
||||
}
|
||||
|
||||
func createDeviceFiles(ctx context.Context, creds *auth.Credentials, info *containerInfo, vfsObj *vfs.VirtualFilesystem, root vfs.VirtualDentry) error {
|
||||
|
||||
@@ -27,7 +27,6 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
@@ -214,56 +213,3 @@ func TestOverlayRootfsWhiteout(t *testing.T) {
|
||||
t.Errorf("root directory contains a file/directory whose name contains %q: output = %q", boot.SelfFilestorePrefix, got)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(b/271612187): Once S/R support is added for file-backed overlays, move
|
||||
// this test to integration_test.go so it can run with the default runsc
|
||||
// configuration which uses file-backed overlay.
|
||||
func TestCheckpointRestore(t *testing.T) {
|
||||
if !testutil.IsCheckpointSupported() {
|
||||
t.Skip("Pause/resume is not supported.")
|
||||
}
|
||||
dockerutil.EnsureDockerExperimentalEnabled()
|
||||
|
||||
ctx := context.Background()
|
||||
d := dockerutil.MakeContainerWithRuntime(ctx, t, "-no-overlay")
|
||||
defer d.CleanUp(ctx)
|
||||
|
||||
// Start the container.
|
||||
port := 8080
|
||||
if err := d.Spawn(ctx, dockerutil.RunOpts{
|
||||
Image: "basic/python",
|
||||
Ports: []int{port}, // See Dockerfile.
|
||||
}); err != nil {
|
||||
t.Fatalf("docker run failed: %v", err)
|
||||
}
|
||||
|
||||
// Create a snapshot.
|
||||
if err := d.Checkpoint(ctx, "test"); err != nil {
|
||||
t.Fatalf("docker checkpoint failed: %v", err)
|
||||
}
|
||||
if err := d.WaitTimeout(ctx, defaultWait); err != nil {
|
||||
t.Fatalf("wait failed: %v", err)
|
||||
}
|
||||
|
||||
// TODO(b/143498576): Remove Poll after github.com/moby/moby/issues/38963 is fixed.
|
||||
if err := testutil.Poll(func() error { return d.Restore(ctx, "test") }, defaultWait); err != nil {
|
||||
t.Fatalf("docker restore failed: %v", err)
|
||||
}
|
||||
|
||||
// Find container IP address.
|
||||
ip, err := d.FindIP(ctx, false)
|
||||
if err != nil {
|
||||
t.Fatalf("docker.FindIP failed: %v", err)
|
||||
}
|
||||
|
||||
// Wait until it's up and running.
|
||||
if err := testutil.WaitForHTTP(ip.String(), port, defaultWait); err != nil {
|
||||
t.Fatalf("WaitForHTTP() timeout: %v", err)
|
||||
}
|
||||
|
||||
// Check if container is working again.
|
||||
client := http.Client{Timeout: defaultWait}
|
||||
if err := testutil.HTTPRequestSucceeds(client, ip.String(), port); err != nil {
|
||||
t.Error("http request failed:", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,20 @@ func TestMain(m *testing.M) {
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
// httpRequestSucceeds sends a request to a given url and checks that the status is OK.
|
||||
func httpRequestSucceeds(client http.Client, server string, port int) error {
|
||||
url := fmt.Sprintf("http://%s:%d", server, port)
|
||||
// Ensure that content is being served.
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reaching http server: %v", err)
|
||||
}
|
||||
if want := http.StatusOK; resp.StatusCode != want {
|
||||
return fmt.Errorf("wrong response code, got: %d, want: %d", resp.StatusCode, want)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestLifeCycle tests a basic Create/Start/Stop docker container life cycle.
|
||||
func TestLifeCycle(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
@@ -83,7 +97,7 @@ func TestLifeCycle(t *testing.T) {
|
||||
t.Fatalf("WaitForHTTP() timeout: %v", err)
|
||||
}
|
||||
client := http.Client{Timeout: defaultWait}
|
||||
if err := testutil.HTTPRequestSucceeds(client, ip.String(), port); err != nil {
|
||||
if err := httpRequestSucceeds(client, ip.String(), port); err != nil {
|
||||
t.Errorf("http request failed: %v", err)
|
||||
}
|
||||
|
||||
@@ -126,7 +140,7 @@ func TestPauseResume(t *testing.T) {
|
||||
|
||||
// Check that container is working.
|
||||
client := http.Client{Timeout: defaultWait}
|
||||
if err := testutil.HTTPRequestSucceeds(client, ip.String(), port); err != nil {
|
||||
if err := httpRequestSucceeds(client, ip.String(), port); err != nil {
|
||||
t.Error("http request failed:", err)
|
||||
}
|
||||
|
||||
@@ -158,7 +172,57 @@ func TestPauseResume(t *testing.T) {
|
||||
|
||||
// Check if container is working again.
|
||||
client = http.Client{Timeout: defaultWait}
|
||||
if err := testutil.HTTPRequestSucceeds(client, ip.String(), port); err != nil {
|
||||
if err := httpRequestSucceeds(client, ip.String(), port); err != nil {
|
||||
t.Error("http request failed:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckpointRestore(t *testing.T) {
|
||||
if !testutil.IsCheckpointSupported() {
|
||||
t.Skip("Pause/resume is not supported.")
|
||||
}
|
||||
dockerutil.EnsureDockerExperimentalEnabled()
|
||||
|
||||
ctx := context.Background()
|
||||
d := dockerutil.MakeContainer(ctx, t)
|
||||
defer d.CleanUp(ctx)
|
||||
|
||||
// Start the container.
|
||||
port := 8080
|
||||
if err := d.Spawn(ctx, dockerutil.RunOpts{
|
||||
Image: "basic/python",
|
||||
Ports: []int{port}, // See Dockerfile.
|
||||
}); err != nil {
|
||||
t.Fatalf("docker run failed: %v", err)
|
||||
}
|
||||
|
||||
// Create a snapshot.
|
||||
if err := d.Checkpoint(ctx, "test"); err != nil {
|
||||
t.Fatalf("docker checkpoint failed: %v", err)
|
||||
}
|
||||
if err := d.WaitTimeout(ctx, defaultWait); err != nil {
|
||||
t.Fatalf("wait failed: %v", err)
|
||||
}
|
||||
|
||||
// TODO(b/143498576): Remove Poll after github.com/moby/moby/issues/38963 is fixed.
|
||||
if err := testutil.Poll(func() error { return d.Restore(ctx, "test") }, defaultWait); err != nil {
|
||||
t.Fatalf("docker restore failed: %v", err)
|
||||
}
|
||||
|
||||
// Find container IP address.
|
||||
ip, err := d.FindIP(ctx, false)
|
||||
if err != nil {
|
||||
t.Fatalf("docker.FindIP failed: %v", err)
|
||||
}
|
||||
|
||||
// Wait until it's up and running.
|
||||
if err := testutil.WaitForHTTP(ip.String(), port, defaultWait); err != nil {
|
||||
t.Fatalf("WaitForHTTP() timeout: %v", err)
|
||||
}
|
||||
|
||||
// Check if container is working again.
|
||||
client := http.Client{Timeout: defaultWait}
|
||||
if err := httpRequestSucceeds(client, ip.String(), port); err != nil {
|
||||
t.Error("http request failed:", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user