diff options
Diffstat (limited to 'libpod')
-rw-r--r-- | libpod/container_api.go | 30 | ||||
-rw-r--r-- | libpod/container_easyjson.go | 2 | ||||
-rw-r--r-- | libpod/container_internal.go | 111 | ||||
-rw-r--r-- | libpod/container_internal_linux.go | 158 | ||||
-rw-r--r-- | libpod/container_internal_unsupported.go | 8 | ||||
-rw-r--r-- | libpod/oci.go | 83 | ||||
-rw-r--r-- | libpod/oci_linux.go | 6 | ||||
-rw-r--r-- | libpod/oci_unsupported.go | 2 |
8 files changed, 315 insertions, 85 deletions
diff --git a/libpod/container_api.go b/libpod/container_api.go index 192ccd347..93becb80d 100644 --- a/libpod/container_api.go +++ b/libpod/container_api.go @@ -832,3 +832,33 @@ func (c *Container) Refresh(ctx context.Context) error { return nil } + +// Checkpoint checkpoints a container +func (c *Container) Checkpoint(ctx context.Context, keep bool) error { + logrus.Debugf("Trying to checkpoint container %s", c) + if !c.batched { + c.lock.Lock() + defer c.lock.Unlock() + + if err := c.syncContainer(); err != nil { + return err + } + } + + return c.checkpoint(ctx, keep) +} + +// Restore restores a container +func (c *Container) Restore(ctx context.Context, keep bool) (err error) { + logrus.Debugf("Trying to restore container %s", c) + if !c.batched { + c.lock.Lock() + defer c.lock.Unlock() + + if err := c.syncContainer(); err != nil { + return err + } + } + + return c.restore(ctx, keep) +} diff --git a/libpod/container_easyjson.go b/libpod/container_easyjson.go index 2d0481f3b..916118aec 100644 --- a/libpod/container_easyjson.go +++ b/libpod/container_easyjson.go @@ -1,3 +1,5 @@ +// +build seccomp ostree selinux varlink exclude_graphdriver_devicemapper + // Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. package libpod diff --git a/libpod/container_internal.go b/libpod/container_internal.go index 033426817..77bba9e85 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -15,9 +15,9 @@ import ( "github.com/containers/libpod/pkg/chrootuser" "github.com/containers/libpod/pkg/hooks" "github.com/containers/libpod/pkg/hooks/exec" + "github.com/containers/libpod/pkg/resolvconf" "github.com/containers/libpod/pkg/rootless" "github.com/containers/libpod/pkg/secrets" - "github.com/containers/libpod/pkg/util" "github.com/containers/storage" "github.com/containers/storage/pkg/archive" "github.com/containers/storage/pkg/chrootarchive" @@ -129,6 +129,11 @@ func (c *Container) ControlSocketPath() string { return filepath.Join(c.bundlePath(), "ctl") } +// CheckpointPath returns the path to the directory containing the checkpoint +func (c *Container) CheckpointPath() string { + return filepath.Join(c.bundlePath(), "checkpoint") +} + // AttachSocketPath retrieves the path of the container's attach socket func (c *Container) AttachSocketPath() string { return filepath.Join(c.runtime.ociRuntime.socketsDir, c.ID(), "attach") @@ -523,7 +528,7 @@ func (c *Container) init(ctx context.Context) error { } // With the spec complete, do an OCI create - if err := c.runtime.ociRuntime.createContainer(c, c.config.CgroupParent); err != nil { + if err := c.runtime.ociRuntime.createContainer(c, c.config.CgroupParent, false); err != nil { return err } @@ -1012,12 +1017,6 @@ func (c *Container) writeStringToRundir(destFile, output string) (string, error) return filepath.Join(c.state.DestinationRunDir, destFile), nil } -type resolvConf struct { - nameServers []string - searchDomains []string - options []string -} - // generateResolvConf generates a containers resolv.conf func (c *Container) generateResolvConf() (string, error) { // Determine the endpoint for resolv.conf in case it is a symlink @@ -1025,86 +1024,56 @@ func (c *Container) generateResolvConf() (string, error) { if err != nil { return "", err } - orig, err := ioutil.ReadFile(resolvPath) + + contents, err := ioutil.ReadFile(resolvPath) if err != nil { return "", errors.Wrapf(err, "unable to read %s", resolvPath) } - if len(c.config.DNSServer) == 0 && len(c.config.DNSSearch) == 0 && len(c.config.DNSOption) == 0 { - return c.writeStringToRundir("resolv.conf", fmt.Sprintf("%s", orig)) - } - // Read and organize the hosts /etc/resolv.conf - resolv := createResolv(string(orig[:])) - - // Populate the resolv struct with user's dns search domains - if len(c.config.DNSSearch) > 0 { - resolv.searchDomains = nil - // The . character means the user doesnt want any search domains in the container - if !util.StringInSlice(".", c.config.DNSSearch) { - resolv.searchDomains = append(resolv.searchDomains, c.Config().DNSSearch...) - } + // Process the file to remove localhost nameservers + // TODO: set ipv6 enable bool more sanely + resolv, err := resolvconf.FilterResolvDNS(contents, true) + if err != nil { + return "", errors.Wrapf(err, "error parsing host resolv.conf") } - // Populate the resolv struct with user's dns servers + // Make a new resolv.conf + nameservers := resolvconf.GetNameservers(resolv.Content) if len(c.config.DNSServer) > 0 { - resolv.nameServers = nil - for _, i := range c.config.DNSServer { - resolv.nameServers = append(resolv.nameServers, i.String()) + // We store DNS servers as net.IP, so need to convert to string + nameservers = []string{} + for _, server := range c.config.DNSServer { + nameservers = append(nameservers, server.String()) } } - // Populate the resolve struct with the users dns options + search := resolvconf.GetSearchDomains(resolv.Content) + if len(c.config.DNSSearch) > 0 { + search = c.config.DNSSearch + } + + options := resolvconf.GetOptions(resolv.Content) if len(c.config.DNSOption) > 0 { - resolv.options = nil - resolv.options = append(resolv.options, c.Config().DNSOption...) + options = c.config.DNSOption } - return c.writeStringToRundir("resolv.conf", resolv.ToString()) -} -// createResolv creates a resolv struct from an input string -func createResolv(input string) resolvConf { - var resolv resolvConf - for _, line := range strings.Split(input, "\n") { - if strings.HasPrefix(line, "search") { - fields := strings.Fields(line) - if len(fields) < 2 { - logrus.Debugf("invalid resolv.conf line %s", line) - continue - } - resolv.searchDomains = append(resolv.searchDomains, fields[1:]...) - } else if strings.HasPrefix(line, "nameserver") { - fields := strings.Fields(line) - if len(fields) < 2 { - logrus.Debugf("invalid resolv.conf line %s", line) - continue - } - resolv.nameServers = append(resolv.nameServers, fields[1]) - } else if strings.HasPrefix(line, "options") { - fields := strings.Fields(line) - if len(fields) < 2 { - logrus.Debugf("invalid resolv.conf line %s", line) - continue - } - resolv.options = append(resolv.options, fields[1:]...) - } + destPath := filepath.Join(c.state.RunDir, "resolv.conf") + + if err := os.Remove(destPath); err != nil && !os.IsNotExist(err) { + return "", errors.Wrapf(err, "error removing resolv.conf for container %s", c.ID()) } - return resolv -} -//ToString returns a resolv struct in the form of a resolv.conf -func (r resolvConf) ToString() string { - var result string - // Populate the output string with search domains - result += fmt.Sprintf("search %s\n", strings.Join(r.searchDomains, " ")) - // Populate the output string with name servers - for _, i := range r.nameServers { - result += fmt.Sprintf("nameserver %s\n", i) + // Build resolv.conf + if _, err = resolvconf.Build(destPath, nameservers, search, options); err != nil { + return "", errors.Wrapf(err, "error building resolv.conf for container %s") } - // Populate the output string with dns options - for _, i := range r.options { - result += fmt.Sprintf("options %s\n", i) + + // Relabel resolv.conf for the container + if err := label.Relabel(destPath, c.config.MountLabel, false); err != nil { + return "", err } - return result + + return filepath.Join(c.state.DestinationRunDir, "resolv.conf"), nil } // generateHosts creates a containers hosts file diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go index b77beaf64..0353124dd 100644 --- a/libpod/container_internal_linux.go +++ b/libpod/container_internal_linux.go @@ -4,12 +4,18 @@ package libpod import ( "context" + "encoding/json" "fmt" + "io/ioutil" + "net" + "os" "path" + "path/filepath" "strings" "syscall" "time" + cnitypes "github.com/containernetworking/cni/pkg/types/current" crioAnnotations "github.com/containers/libpod/pkg/annotations" "github.com/containers/libpod/pkg/chrootuser" "github.com/containers/libpod/pkg/rootless" @@ -307,3 +313,155 @@ func (c *Container) addNamespaceContainer(g *generate.Generator, ns LinuxNS, ctr return nil } + +func (c *Container) checkpoint(ctx context.Context, keep bool) (err error) { + + if c.state.State != ContainerStateRunning { + return errors.Wrapf(ErrCtrStateInvalid, "%q is not running, cannot checkpoint", c.state.State) + } + if err := c.runtime.ociRuntime.checkpointContainer(c); err != nil { + return err + } + + // Save network.status. This is needed to restore the container with + // the same IP. Currently limited to one IP address in a container + // with one interface. + formatJSON, err := json.MarshalIndent(c.state.NetworkStatus, "", " ") + if err != nil { + return err + } + if err := ioutil.WriteFile(filepath.Join(c.bundlePath(), "network.status"), formatJSON, 0644); err != nil { + return err + } + + logrus.Debugf("Checkpointed container %s", c.ID()) + + c.state.State = ContainerStateStopped + + // Cleanup Storage and Network + if err := c.cleanup(ctx); err != nil { + return err + } + + if !keep { + // Remove log file + os.Remove(filepath.Join(c.bundlePath(), "dump.log")) + // Remove statistic file + os.Remove(filepath.Join(c.bundlePath(), "stats-dump")) + } + + return c.save() +} + +func (c *Container) restore(ctx context.Context, keep bool) (err error) { + + if (c.state.State != ContainerStateConfigured) && (c.state.State != ContainerStateExited) { + return errors.Wrapf(ErrCtrStateInvalid, "container %s is running or paused, cannot restore", c.ID()) + } + + // Let's try to stat() CRIU's inventory file. If it does not exist, it makes + // no sense to try a restore. This is a minimal check if a checkpoint exist. + if _, err := os.Stat(filepath.Join(c.CheckpointPath(), "inventory.img")); os.IsNotExist(err) { + return errors.Wrapf(err, "A complete checkpoint for this container cannot be found, cannot restore") + } + + // 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 { + // The file with the network.status does exist. Let's restore the + // container with the same IP address as during checkpointing. + defer networkStatusFile.Close() + var networkStatus []*cnitypes.Result + networkJSON, err := ioutil.ReadAll(networkStatusFile) + if err != nil { + return err + } + json.Unmarshal(networkJSON, &networkStatus) + // Take the first IP address + var IP net.IP + if len(networkStatus) > 0 { + if len(networkStatus[0].IPs) > 0 { + IP = networkStatus[0].IPs[0].Address.IP + } + } + if IP != nil { + env := fmt.Sprintf("IP=%s", IP) + // Tell CNI which IP address we want. + os.Setenv("CNI_ARGS", env) + logrus.Debugf("Restoring container with %s", env) + } + } + + if err := c.prepare(); err != nil { + return err + } + defer func() { + if err != nil { + if err2 := c.cleanup(ctx); err2 != nil { + logrus.Errorf("error cleaning up container %s: %v", c.ID(), err2) + } + } + }() + + // TODO: use existing way to request static IPs, once it is merged in ocicni + // https://github.com/cri-o/ocicni/pull/23/ + + // CNI_ARGS was used to request a certain IP address. Unconditionally remove it. + os.Unsetenv("CNI_ARGS") + + // Read config + jsonPath := filepath.Join(c.bundlePath(), "config.json") + logrus.Debugf("generate.NewFromFile at %v", jsonPath) + g, err := generate.NewFromFile(jsonPath) + if err != nil { + logrus.Debugf("generate.NewFromFile failed with %v", err) + return err + } + + // We want to have the same network namespace as before. + if c.config.CreateNetNS { + g.AddOrReplaceLinuxNamespace(spec.NetworkNamespace, c.state.NetNS.Path()) + } + + // Save the OCI spec to disk + if err := c.saveSpec(g.Spec()); err != nil { + return err + } + + if err := c.makeBindMounts(); err != nil { + return err + } + + // Cleanup for a working restore. + c.removeConmonFiles() + + if err := c.runtime.ociRuntime.createContainer(c, c.config.CgroupParent, true); err != nil { + return err + } + + logrus.Debugf("Restored container %s", c.ID()) + + c.state.State = ContainerStateRunning + + if !keep { + // Delete all checkpoint related files. At this point, in theory, all files + // should exist. Still ignoring errors for now as the container should be + // restored and running. Not erroring out just because some cleanup operation + // failed. Starting with the checkpoint directory + err = os.RemoveAll(c.CheckpointPath()) + 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) + err = os.Remove(file) + if err != nil { + logrus.Debugf("Non-fatal: removal of checkpoint file (%s) failed: %v", file, err) + } + } + } + + return c.save() +} diff --git a/libpod/container_internal_unsupported.go b/libpod/container_internal_unsupported.go index 45b54efab..eed0449a9 100644 --- a/libpod/container_internal_unsupported.go +++ b/libpod/container_internal_unsupported.go @@ -27,3 +27,11 @@ func (c *Container) cleanupNetwork() error { func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) { return nil, ErrNotImplemented } + +func (c *Container) checkpoint(ctx context.Context, keep bool) error { + return ErrNotImplemented +} + +func (c *Container) restore(ctx context.Context, keep bool) error { + return ErrNotImplemented +} diff --git a/libpod/oci.go b/libpod/oci.go index e5db06540..f6d320017 100644 --- a/libpod/oci.go +++ b/libpod/oci.go @@ -227,7 +227,7 @@ func bindPorts(ports []ocicni.PortMapping) ([]*os.File, error) { return files, nil } -func (r *OCIRuntime) createOCIContainer(ctr *Container, cgroupParent string) (err error) { +func (r *OCIRuntime) createOCIContainer(ctr *Container, cgroupParent string, restoreContainer bool) (err error) { var stderrBuf bytes.Buffer runtimeDir, err := GetRootlessRuntimeDir() @@ -289,6 +289,10 @@ func (r *OCIRuntime) createOCIContainer(ctr *Container, cgroupParent string) (er args = append(args, "--syslog") } + if restoreContainer { + args = append(args, "--restore", ctr.CheckpointPath()) + } + logrus.WithFields(logrus.Fields{ "args": args, }).Debugf("running conmon: %s", r.conmonPath) @@ -452,9 +456,20 @@ func (r *OCIRuntime) updateContainerStatus(ctr *Container) error { cmd := exec.Command(r.path, "state", ctr.ID()) cmd.Env = append(cmd.Env, fmt.Sprintf("XDG_RUNTIME_DIR=%s", runtimeDir)) - - out, err := cmd.CombinedOutput() + outPipe, err := cmd.StdoutPipe() + if err != nil { + return errors.Wrapf(err, "getting stdout pipe") + } + errPipe, err := cmd.StderrPipe() if err != nil { + return errors.Wrapf(err, "getting stderr pipe") + } + + if err := cmd.Start(); err != nil { + out, err2 := ioutil.ReadAll(errPipe) + if err2 != nil { + return errors.Wrapf(err, "error getting container %s state", ctr.ID()) + } if strings.Contains(string(out), "does not exist") { ctr.removeConmonFiles() ctr.state.State = ContainerStateExited @@ -462,6 +477,12 @@ func (r *OCIRuntime) updateContainerStatus(ctr *Container) error { } return errors.Wrapf(err, "error getting container %s state. stderr/out: %s", ctr.ID(), out) } + + errPipe.Close() + out, err := ioutil.ReadAll(outPipe) + if err != nil { + return errors.Wrapf(err, "error reading stdout: %s", ctr.ID()) + } if err := json.NewDecoder(bytes.NewBuffer(out)).Decode(state); err != nil { return errors.Wrapf(err, "error decoding container status for container %s", ctr.ID()) } @@ -535,7 +556,12 @@ func (r *OCIRuntime) updateContainerStatus(ctr *Container) error { // Sets time the container was started, but does not save it. func (r *OCIRuntime) startContainer(ctr *Container) error { // TODO: streams should probably *not* be our STDIN/OUT/ERR - redirect to buffers? - if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, nil, r.path, "start", ctr.ID()); err != nil { + runtimeDir, err := GetRootlessRuntimeDir() + if err != nil { + return err + } + env := []string{fmt.Sprintf("XDG_RUNTIME_DIR=%s", runtimeDir)} + if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, env, r.path, "start", ctr.ID()); err != nil { return err } @@ -547,7 +573,12 @@ func (r *OCIRuntime) startContainer(ctr *Container) error { // killContainer sends the given signal to the given container func (r *OCIRuntime) killContainer(ctr *Container, signal uint) error { logrus.Debugf("Sending signal %d to container %s", signal, ctr.ID()) - if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, nil, r.path, "kill", ctr.ID(), fmt.Sprintf("%d", signal)); err != nil { + runtimeDir, err := GetRootlessRuntimeDir() + if err != nil { + return err + } + env := []string{fmt.Sprintf("XDG_RUNTIME_DIR=%s", runtimeDir)} + if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, env, r.path, "kill", ctr.ID(), fmt.Sprintf("%d", signal)); err != nil { return errors.Wrapf(err, "error sending signal to container %s", ctr.ID()) } @@ -605,7 +636,12 @@ func (r *OCIRuntime) stopContainer(ctr *Container, timeout uint) error { args = []string{"kill", "--all", ctr.ID(), "KILL"} } - if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, nil, r.path, args...); err != nil { + runtimeDir, err := GetRootlessRuntimeDir() + if err != nil { + return err + } + env := []string{fmt.Sprintf("XDG_RUNTIME_DIR=%s", runtimeDir)} + if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, env, r.path, args...); err != nil { // Again, check if the container is gone. If it is, exit cleanly. err := unix.Kill(ctr.state.PID, 0) if err == unix.ESRCH { @@ -631,12 +667,22 @@ func (r *OCIRuntime) deleteContainer(ctr *Container) error { // pauseContainer pauses the given container func (r *OCIRuntime) pauseContainer(ctr *Container) error { - return utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, nil, r.path, "pause", ctr.ID()) + runtimeDir, err := GetRootlessRuntimeDir() + if err != nil { + return err + } + env := []string{fmt.Sprintf("XDG_RUNTIME_DIR=%s", runtimeDir)} + return utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, env, r.path, "pause", ctr.ID()) } // unpauseContainer unpauses the given container func (r *OCIRuntime) unpauseContainer(ctr *Container) error { - return utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, nil, r.path, "resume", ctr.ID()) + runtimeDir, err := GetRootlessRuntimeDir() + if err != nil { + return err + } + env := []string{fmt.Sprintf("XDG_RUNTIME_DIR=%s", runtimeDir)} + return utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, env, r.path, "resume", ctr.ID()) } // execContainer executes a command in a running container @@ -734,13 +780,18 @@ func (r *OCIRuntime) execStopContainer(ctr *Container, timeout uint) error { if len(execSessions) == 0 { return nil } + runtimeDir, err := GetRootlessRuntimeDir() + if err != nil { + return err + } + env := []string{fmt.Sprintf("XDG_RUNTIME_DIR=%s", runtimeDir)} // If timeout is 0, just use SIGKILL if timeout > 0 { // Stop using SIGTERM by default // Use SIGSTOP after a timeout logrus.Debugf("Killing all processes in container %s with SIGTERM", ctr.ID()) - if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, nil, r.path, "kill", "--all", ctr.ID(), "TERM"); err != nil { + if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, env, r.path, "kill", "--all", ctr.ID(), "TERM"); err != nil { return errors.Wrapf(err, "error sending SIGTERM to container %s processes", ctr.ID()) } @@ -755,7 +806,7 @@ func (r *OCIRuntime) execStopContainer(ctr *Container, timeout uint) error { // Send SIGKILL logrus.Debugf("Killing all processes in container %s with SIGKILL", ctr.ID()) - if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, nil, r.path, "kill", "--all", ctr.ID(), "KILL"); err != nil { + if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, env, r.path, "kill", "--all", ctr.ID(), "KILL"); err != nil { return errors.Wrapf(err, "error sending SIGKILL to container %s processes", ctr.ID()) } @@ -766,3 +817,15 @@ func (r *OCIRuntime) execStopContainer(ctr *Container, timeout uint) error { return nil } + +// checkpointContainer checkpoints the given container +func (r *OCIRuntime) checkpointContainer(ctr *Container) error { + // imagePath is used by CRIU to store the actual checkpoint files + imagePath := ctr.CheckpointPath() + // workPath will be used to store dump.log and stats-dump + workPath := ctr.bundlePath() + logrus.Debugf("Writing checkpoint to %s", imagePath) + logrus.Debugf("Writing checkpoint logs to %s", workPath) + return utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, nil, r.path, "checkpoint", + "--image-path", imagePath, "--work-path", workPath, ctr.ID()) +} diff --git a/libpod/oci_linux.go b/libpod/oci_linux.go index 210ba57d1..0447670b3 100644 --- a/libpod/oci_linux.go +++ b/libpod/oci_linux.go @@ -63,10 +63,10 @@ func newPipe() (parent *os.File, child *os.File, err error) { // CreateContainer creates a container in the OCI runtime // TODO terminal support for container // Presently just ignoring conmon opts related to it -func (r *OCIRuntime) createContainer(ctr *Container, cgroupParent string) (err error) { +func (r *OCIRuntime) createContainer(ctr *Container, cgroupParent string, restoreContainer bool) (err error) { if ctr.state.UserNSRoot == "" { // no need of an intermediate mount ns - return r.createOCIContainer(ctr, cgroupParent) + return r.createOCIContainer(ctr, cgroupParent, restoreContainer) } var wg sync.WaitGroup wg.Add(1) @@ -103,7 +103,7 @@ func (r *OCIRuntime) createContainer(ctr *Container, cgroupParent string) (err e if err != nil { return } - err = r.createOCIContainer(ctr, cgroupParent) + err = r.createOCIContainer(ctr, cgroupParent, restoreContainer) }() wg.Wait() diff --git a/libpod/oci_unsupported.go b/libpod/oci_unsupported.go index 8cb4994d3..b133eb402 100644 --- a/libpod/oci_unsupported.go +++ b/libpod/oci_unsupported.go @@ -15,7 +15,7 @@ func newPipe() (parent *os.File, child *os.File, err error) { return nil, nil, ErrNotImplemented } -func (r *OCIRuntime) createContainer(ctr *Container, cgroupParent string) (err error) { +func (r *OCIRuntime) createContainer(ctr *Container, cgroupParent string, restoreContainer bool) (err error) { return ErrNotImplemented } |