diff options
Diffstat (limited to 'libpod/container_internal_linux.go')
-rw-r--r-- | libpod/container_internal_linux.go | 277 |
1 files changed, 233 insertions, 44 deletions
diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go index f25f76092..afcf51a11 100644 --- a/libpod/container_internal_linux.go +++ b/libpod/container_internal_linux.go @@ -5,6 +5,7 @@ package libpod import ( "context" "fmt" + "io" "io/ioutil" "net" "os" @@ -19,12 +20,15 @@ import ( cnitypes "github.com/containernetworking/cni/pkg/types/current" "github.com/containernetworking/plugins/pkg/ns" "github.com/containers/buildah/pkg/secrets" + "github.com/containers/libpod/libpod/define" crioAnnotations "github.com/containers/libpod/pkg/annotations" "github.com/containers/libpod/pkg/apparmor" + "github.com/containers/libpod/pkg/cgroups" "github.com/containers/libpod/pkg/criu" "github.com/containers/libpod/pkg/lookup" "github.com/containers/libpod/pkg/resolvconf" "github.com/containers/libpod/pkg/rootless" + "github.com/containers/storage/pkg/archive" securejoin "github.com/cyphar/filepath-securejoin" "github.com/opencontainers/runc/libcontainer/user" spec "github.com/opencontainers/runtime-spec/specs-go" @@ -181,9 +185,13 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) { // If network namespace was requested, add it now if c.config.CreateNetNS { if c.config.PostConfigureNetNS { - g.AddOrReplaceLinuxNamespace(spec.NetworkNamespace, "") + if err := g.AddOrReplaceLinuxNamespace(spec.NetworkNamespace, ""); err != nil { + return nil, err + } } else { - g.AddOrReplaceLinuxNamespace(spec.NetworkNamespace, c.state.NetNS.Path()) + if err := g.AddOrReplaceLinuxNamespace(spec.NetworkNamespace, c.state.NetNS.Path()); err != nil { + return nil, err + } } } @@ -310,6 +318,11 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) { if err := c.addNamespaceContainer(&g, UserNS, c.config.UserNsCtr, spec.UserNamespace); err != nil { return nil, err } + if len(g.Config.Linux.UIDMappings) == 0 { + // runc complains if no mapping is specified, even if we join another ns. So provide a dummy mapping + g.AddLinuxUIDMapping(uint32(0), uint32(0), uint32(1)) + g.AddLinuxGIDMapping(uint32(0), uint32(0), uint32(1)) + } } if c.config.UTSNsCtr != "" { if err := c.addNamespaceContainer(&g, UTSNS, c.config.UTSNsCtr, spec.UTSNamespace); err != nil { @@ -347,7 +360,11 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) { g.AddProcessEnv("container", "libpod") } - if rootless.IsRootless() { + unified, err := cgroups.IsCgroup2UnifiedMode() + if err != nil { + return nil, err + } + if rootless.IsRootless() && !unified { g.SetLinuxCgroupsPath("") } else if c.runtime.config.CgroupManager == SystemdCgroupsManager { // When runc is set to use Systemd as a cgroup manager, it @@ -407,7 +424,9 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) { if rootPropagation != "" { logrus.Debugf("set root propagation to %q", rootPropagation) - g.SetLinuxRootPropagation(rootPropagation) + if err := g.SetLinuxRootPropagation(rootPropagation); err != nil { + return nil, err + } } // Warning: precreate hooks may alter g.Config in place. @@ -422,7 +441,7 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) { // It also expects to be able to write to /sys/fs/cgroup/systemd and /var/log/journal func (c *Container) setupSystemd(mounts []spec.Mount, g generate.Generator) error { options := []string{"rw", "rprivate", "noexec", "nosuid", "nodev"} - for _, dest := range []string{"/run"} { + for _, dest := range []string{"/run", "/run/lock"} { if MountExists(mounts, dest) { continue } @@ -496,11 +515,75 @@ func (c *Container) addNamespaceContainer(g *generate.Generator, ns LinuxNS, ctr return nil } +func (c *Container) exportCheckpoint(dest string, ignoreRootfs bool) (err error) { + if (len(c.config.NamedVolumes) > 0) || (len(c.Dependencies()) > 0) { + return errors.Errorf("Cannot export checkpoints of containers with named volumes or dependencies") + } + logrus.Debugf("Exporting checkpoint image of container %q to %q", c.ID(), dest) + + includeFiles := []string{ + "checkpoint", + "artifacts", + "ctr.log", + "config.dump", + "spec.dump", + "network.status"} + + // Get root file-system changes included in the checkpoint archive + rootfsDiffPath := filepath.Join(c.bundlePath(), "rootfs-diff.tar") + if !ignoreRootfs { + rootfsDiffFile, err := os.Create(rootfsDiffPath) + if err != nil { + return errors.Wrapf(err, "error creating root file-system diff file %q", rootfsDiffPath) + } + tarStream, err := c.runtime.GetDiffTarStream("", c.ID()) + if err != nil { + return errors.Wrapf(err, "error exporting root file-system diff to %q", rootfsDiffPath) + } + _, err = io.Copy(rootfsDiffFile, tarStream) + if err != nil { + return errors.Wrapf(err, "error exporting root file-system diff to %q", rootfsDiffPath) + } + tarStream.Close() + rootfsDiffFile.Close() + includeFiles = append(includeFiles, "rootfs-diff.tar") + } + + input, err := archive.TarWithOptions(c.bundlePath(), &archive.TarOptions{ + Compression: archive.Gzip, + IncludeSourceDir: true, + IncludeFiles: includeFiles, + }) + + if err != nil { + return errors.Wrapf(err, "error reading checkpoint directory %q", c.ID()) + } + + outFile, err := os.Create(dest) + if err != nil { + return errors.Wrapf(err, "error creating checkpoint export file %q", dest) + } + defer outFile.Close() + + if err := os.Chmod(dest, 0600); err != nil { + return errors.Wrapf(err, "cannot chmod %q", dest) + } + + _, err = io.Copy(outFile, input) + if err != nil { + return err + } + + os.Remove(rootfsDiffPath) + + return nil +} + func (c *Container) checkpointRestoreSupported() (err error) { if !criu.CheckForCriu() { return errors.Errorf("Checkpoint/Restore requires at least CRIU %d", criu.MinCriuVersion) } - if !c.runtime.ociRuntime.featureCheckCheckpointing() { + if !c.ociRuntime.featureCheckCheckpointing() { return errors.Errorf("Configured runtime does not support checkpoint/restore") } return nil @@ -514,7 +597,9 @@ func (c *Container) checkpointRestoreLabelLog(fileName string) (err error) { if err != nil { return errors.Wrapf(err, "failed to create CRIU log file %q", dumpLog) } - logFile.Close() + if err := logFile.Close(); err != nil { + logrus.Errorf("unable to close log file: %q", err) + } if err = label.SetFileLabel(dumpLog, c.MountLabel()); err != nil { return errors.Wrapf(err, "failed to label CRIU log file %q", dumpLog) } @@ -526,15 +611,15 @@ func (c *Container) checkpoint(ctx context.Context, options ContainerCheckpointO return err } - if c.state.State != ContainerStateRunning { - return errors.Wrapf(ErrCtrStateInvalid, "%q is not running, cannot checkpoint", c.state.State) + if c.state.State != define.ContainerStateRunning { + return errors.Wrapf(define.ErrCtrStateInvalid, "%q is not running, cannot checkpoint", c.state.State) } if err := c.checkpointRestoreLabelLog("dump.log"); err != nil { return err } - if err := c.runtime.ociRuntime.checkpointContainer(c, options); err != nil { + if err := c.ociRuntime.checkpointContainer(c, options); err != nil { return err } @@ -549,10 +634,16 @@ func (c *Container) checkpoint(ctx context.Context, options ContainerCheckpointO return err } + if options.TargetFile != "" { + if err = c.exportCheckpoint(options.TargetFile, options.IgnoreRootfs); err != nil { + return err + } + } + logrus.Debugf("Checkpointed container %s", c.ID()) if !options.KeepRunning { - c.state.State = ContainerStateStopped + c.state.State = define.ContainerStateStopped // Cleanup Storage and Network if err := c.cleanup(ctx); err != nil { @@ -561,23 +652,67 @@ func (c *Container) checkpoint(ctx context.Context, options ContainerCheckpointO } if !options.Keep { - // Remove log file - os.Remove(filepath.Join(c.bundlePath(), "dump.log")) - // Remove statistic file - os.Remove(filepath.Join(c.bundlePath(), "stats-dump")) + cleanup := []string{ + "dump.log", + "stats-dump", + "config.dump", + "spec.dump", + } + for _, del := range cleanup { + file := filepath.Join(c.bundlePath(), del) + if err := os.Remove(file); err != nil { + logrus.Debugf("unable to remove file %s", file) + } + } } + c.state.FinishedTime = time.Now() return c.save() } +func (c *Container) importCheckpoint(input string) (err error) { + archiveFile, err := os.Open(input) + if err != nil { + return errors.Wrapf(err, "Failed to open checkpoint archive %s for import", input) + } + + defer archiveFile.Close() + options := &archive.TarOptions{ + ExcludePatterns: []string{ + // config.dump and spec.dump are only required + // container creation + "config.dump", + "spec.dump", + }, + } + err = archive.Untar(archiveFile, c.bundlePath(), options) + if err != nil { + return errors.Wrapf(err, "Unpacking of checkpoint archive %s failed", input) + } + + // Make sure the newly created config.json exists on disk + g := generate.Generator{Config: c.config.Spec} + if err = c.saveSpec(g.Config); err != nil { + return errors.Wrap(err, "Saving imported container specification for restore failed") + } + + return nil +} + func (c *Container) restore(ctx context.Context, options ContainerCheckpointOptions) (err error) { if err := c.checkpointRestoreSupported(); err != nil { return err } - if (c.state.State != ContainerStateConfigured) && (c.state.State != ContainerStateExited) { - return errors.Wrapf(ErrCtrStateInvalid, "container %s is running or paused, cannot restore", c.ID()) + if (c.state.State != define.ContainerStateConfigured) && (c.state.State != define.ContainerStateExited) { + return errors.Wrapf(define.ErrCtrStateInvalid, "container %s is running or paused, cannot restore", c.ID()) + } + + if options.TargetFile != "" { + if err = c.importCheckpoint(options.TargetFile); err != nil { + return err + } } // Let's try to stat() CRIU's inventory file. If it does not exist, it makes @@ -593,7 +728,13 @@ func (c *Container) restore(ctx context.Context, options ContainerCheckpointOpti // Read network configuration from checkpoint // Currently only one interface with one IP is supported. networkStatusFile, err := os.Open(filepath.Join(c.bundlePath(), "network.status")) - if err == nil { + // If the restored container should get a new name, the IP address of + // the container will not be restored. This assumes that if a new name is + // specified, the container is restored multiple times. + // TODO: This implicit restoring with or without IP depending on an + // unrelated restore parameter (--name) does not seem like the + // best solution. + if err == nil && options.Name == "" { // The file with the network.status does exist. Let's restore the // container with the same IP address as during checkpointing. defer networkStatusFile.Close() @@ -602,7 +743,9 @@ func (c *Container) restore(ctx context.Context, options ContainerCheckpointOpti if err != nil { return err } - json.Unmarshal(networkJSON, &networkStatus) + if err := json.Unmarshal(networkJSON, &networkStatus); err != nil { + return err + } // Take the first IP address var IP net.IP if len(networkStatus) > 0 { @@ -637,30 +780,72 @@ func (c *Container) restore(ctx context.Context, options ContainerCheckpointOpti return err } + // Restoring from an import means that we are doing migration + if options.TargetFile != "" { + g.SetRootPath(c.state.Mountpoint) + } + // We want to have the same network namespace as before. if c.config.CreateNetNS { - g.AddOrReplaceLinuxNamespace(spec.NetworkNamespace, c.state.NetNS.Path()) + if err := g.AddOrReplaceLinuxNamespace(spec.NetworkNamespace, c.state.NetNS.Path()); err != nil { + return err + } } - // Save the OCI spec to disk - if err := c.saveSpec(g.Spec()); err != nil { + if err := c.makeBindMounts(); err != nil { return err } - if err := c.makeBindMounts(); err != nil { - return err + if options.TargetFile != "" { + for dstPath, srcPath := range c.state.BindMounts { + newMount := spec.Mount{ + Type: "bind", + Source: srcPath, + Destination: dstPath, + Options: []string{"bind", "private"}, + } + if c.IsReadOnly() && dstPath != "/dev/shm" { + newMount.Options = append(newMount.Options, "ro", "nosuid", "noexec", "nodev") + } + if !MountExists(g.Mounts(), dstPath) { + g.AddMount(newMount) + } + } } // Cleanup for a working restore. - c.removeConmonFiles() + if err := c.removeConmonFiles(); err != nil { + return err + } + + // Save the OCI spec to disk + if err := c.saveSpec(g.Config); err != nil { + return err + } - if err := c.runtime.ociRuntime.createContainer(c, c.config.CgroupParent, &options); err != nil { + // Before actually restarting the container, apply the root file-system changes + if !options.IgnoreRootfs { + rootfsDiffPath := filepath.Join(c.bundlePath(), "rootfs-diff.tar") + if _, err := os.Stat(rootfsDiffPath); err == nil { + // Only do this if a rootfs-diff.tar actually exists + rootfsDiffFile, err := os.Open(rootfsDiffPath) + if err != nil { + return errors.Wrapf(err, "Failed to open root file-system diff file %s", rootfsDiffPath) + } + if err := c.runtime.ApplyDiffTarStream(c.ID(), rootfsDiffFile); err != nil { + return errors.Wrapf(err, "Failed to apply root file-system diff file %s", rootfsDiffPath) + } + rootfsDiffFile.Close() + } + } + + if err := c.ociRuntime.createContainer(c, &options); err != nil { return err } logrus.Debugf("Restored container %s", c.ID()) - c.state.State = ContainerStateRunning + c.state.State = define.ContainerStateRunning if !options.Keep { // Delete all checkpoint related files. At this point, in theory, all files @@ -671,9 +856,9 @@ func (c *Container) restore(ctx context.Context, options ContainerCheckpointOpti if err != nil { logrus.Debugf("Non-fatal: removal of checkpoint directory (%s) failed: %v", c.CheckpointPath(), err) } - cleanup := [...]string{"restore.log", "dump.log", "stats-dump", "stats-restore", "network.status"} - for _, delete := range cleanup { - file := filepath.Join(c.bundlePath(), delete) + cleanup := [...]string{"restore.log", "dump.log", "stats-dump", "stats-restore", "network.status", "rootfs-diff.tar"} + for _, del := range cleanup { + file := filepath.Join(c.bundlePath(), del) err = os.Remove(file) if err != nil { logrus.Debugf("Non-fatal: removal of checkpoint file (%s) failed: %v", file, err) @@ -703,14 +888,14 @@ func (c *Container) makeBindMounts() error { // will recreate. Only do this if we aren't sharing them with // another container. if c.config.NetNsCtr == "" { - if path, ok := c.state.BindMounts["/etc/resolv.conf"]; ok { - if err := os.Remove(path); err != nil && !os.IsNotExist(err) { + if resolvePath, ok := c.state.BindMounts["/etc/resolv.conf"]; ok { + if err := os.Remove(resolvePath); err != nil && !os.IsNotExist(err) { return errors.Wrapf(err, "error removing container %s resolv.conf", c.ID()) } delete(c.state.BindMounts, "/etc/resolv.conf") } - if path, ok := c.state.BindMounts["/etc/hosts"]; ok { - if err := os.Remove(path); err != nil && !os.IsNotExist(err) { + if hostsPath, ok := c.state.BindMounts["/etc/hosts"]; ok { + if err := os.Remove(hostsPath); err != nil && !os.IsNotExist(err) { return errors.Wrapf(err, "error removing container %s hosts", c.ID()) } delete(c.state.BindMounts, "/etc/hosts") @@ -847,10 +1032,10 @@ func (c *Container) makeBindMounts() error { // generateResolvConf generates a containers resolv.conf func (c *Container) generateResolvConf() (string, error) { resolvConf := "/etc/resolv.conf" - for _, ns := range c.config.Spec.Linux.Namespaces { - if ns.Type == spec.NetworkNamespace { - if ns.Path != "" && !strings.HasPrefix(ns.Path, "/proc/") { - definedPath := filepath.Join("/etc/netns", filepath.Base(ns.Path), "resolv.conf") + for _, namespace := range c.config.Spec.Linux.Namespaces { + if namespace.Type == spec.NetworkNamespace { + if namespace.Path != "" && !strings.HasPrefix(namespace.Path, "/proc/") { + definedPath := filepath.Join("/etc/netns", filepath.Base(namespace.Path), "resolv.conf") _, err := os.Stat(definedPath) if err == nil { resolvConf = definedPath @@ -885,7 +1070,7 @@ func (c *Container) generateResolvConf() (string, error) { nameservers := resolvconf.GetNameservers(resolv.Content) // slirp4netns has a built in DNS server. if c.config.NetMode.IsSlirp4netns() { - nameservers = append(nameservers, "10.0.2.3") + nameservers = append([]string{"10.0.2.3"}, nameservers...) } if len(c.config.DNSServer) > 0 { // We store DNS servers as net.IP, so need to convert to string @@ -954,6 +1139,10 @@ func (c *Container) getHosts() string { hosts += fmt.Sprintf("%s %s\n", fields[1], fields[0]) } } + if c.config.NetMode.IsSlirp4netns() { + // When using slirp4netns, the interface gets a static IP + hosts += fmt.Sprintf("# used by slirp4netns\n%s\t%s\n", "10.0.2.100", c.Hostname()) + } if len(c.state.NetworkStatus) > 0 && len(c.state.NetworkStatus[0].IPs) > 0 { ipAddress := strings.Split(c.state.NetworkStatus[0].IPs[0].Address.String(), "/")[0] hosts += fmt.Sprintf("%s\t%s\n", ipAddress, c.Hostname()) @@ -971,10 +1160,10 @@ func (c *Container) generatePasswd() (string, error) { if c.config.User == "" { return "", nil } - spec := strings.SplitN(c.config.User, ":", 2) - userspec := spec[0] - if len(spec) > 1 { - groupspec = spec[1] + splitSpec := strings.SplitN(c.config.User, ":", 2) + userspec := splitSpec[0] + if len(splitSpec) > 1 { + groupspec = splitSpec[1] } // If a non numeric User, then don't generate passwd uid, err := strconv.ParseUint(userspec, 10, 32) @@ -1012,7 +1201,7 @@ func (c *Container) generatePasswd() (string, error) { if err != nil { return "", errors.Wrapf(err, "failed to create temporary passwd file") } - if os.Chmod(passwdFile, 0644); err != nil { + if err := os.Chmod(passwdFile, 0644); err != nil { return "", err } return passwdFile, nil |