Merge pull request #1043 from mheon/kpod_container_skeleton

Libpod container creation logic
This commit is contained in:
Daniel J Walsh 2017-10-26 16:26:06 -07:00 committed by GitHub
commit 596a97119b
8 changed files with 1066 additions and 71 deletions

View File

@ -1,52 +1,159 @@
package libpod
import (
"encoding/json"
"io/ioutil"
"path/filepath"
"sync"
"time"
"github.com/containers/storage"
"github.com/docker/docker/pkg/stringid"
crioAnnotations "github.com/kubernetes-incubator/cri-o/pkg/annotations"
spec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/ulule/deepcopier"
)
// ContainerState represents the current state of a container
type ContainerState int
const (
// ContainerStateUnknown indicates that the container is in an error
// state where information about it cannot be retrieved
ContainerStateUnknown ContainerState = iota
// ContainerStateConfigured indicates that the container has had its
// storage configured but it has not been created in the OCI runtime
ContainerStateConfigured ContainerState = iota
// ContainerStateCreated indicates the container has been created in
// the OCI runtime but not started
ContainerStateCreated ContainerState = iota
// ContainerStateRunning indicates the container is currently executing
ContainerStateRunning ContainerState = iota
// ContainerStateStopped indicates that the container was running but has
// exited
ContainerStateStopped ContainerState = iota
// ContainerStatePaused indicates that the container has been paused
ContainerStatePaused ContainerState = iota
)
// Container is a single OCI container
type Container struct {
id string
name string
config *containerConfig
spec *spec.Spec
pod *Pod
pod *Pod
runningSpec *spec.Spec
valid bool
lock sync.RWMutex
state *containerRuntimeInfo
// TODO move to storage.Locker from sync.Mutex
valid bool
lock sync.Mutex
runtime *Runtime
}
// containerState contains the current state of the container
// It is stored on disk in a tmpfs and recreated on reboot
type containerRuntimeInfo struct {
// The current state of the running container
State ContainerState `json:"state"`
// The path to the JSON OCI runtime spec for this container
ConfigPath string `json:"configPath,omitempty"`
// RunDir is a per-boot directory for container content
RunDir string `json:"runDir,omitempty"`
// Mounted indicates whether the container's storage has been mounted
// for use
Mounted bool `json:"-"`
// MountPoint contains the path to the container's mounted storage
Mountpoint string `json:"mountPoint,omitempty"`
// StartedTime is the time the container was started
StartedTime time.Time `json:"startedTime,omitempty"`
// FinishedTime is the time the container finished executing
FinishedTime time.Time `json:"finishedTime,omitempty"`
// ExitCode is the exit code returned when the container stopped
ExitCode int32 `json:"exitCode,omitempty"`
// TODO: Save information about image used in container if one is used
}
// containerConfig contains all information that was used to create the
// container. It may not be changed once created.
// It is stored, read-only, on disk
type containerConfig struct {
Spec *spec.Spec `json:"spec"`
ID string `json:"id"`
Name string `json:"name"`
// RootfsFromImage indicates whether the container uses a root
// filesystem from an image, or from a user-provided directory
RootfsFromImage bool
// Directory used as a root filesystem if not configured with an image
RootfsDir string `json:"rootfsDir,omitempty"`
// Information on the image used for the root filesystem
RootfsImageID string `json:"rootfsImageID,omitempty"`
RootfsImageName string `json:"rootfsImageName,omitempty"`
UseImageConfig bool `json:"useImageConfig"`
// Whether to keep container STDIN open
Stdin bool
// Static directory for container content that will persist across
// reboot
StaticDir string `json:"staticDir"`
// Pod the container belongs to
Pod string `json:"pod,omitempty"`
// Shared namespaces with container
SharedNamespaceCtr *string `json:"shareNamespacesWith,omitempty"`
SharedNamespaceMap map[string]string `json:"sharedNamespaces"`
// Time container was created
CreatedTime time.Time `json:"createdTime"`
// TODO save log location here and pass into OCI code
// TODO allow overriding of log path
}
// ID returns the container's ID
func (c *Container) ID() string {
// No locking needed, ID will never mutate after a container is created
return c.id
return c.config.ID
}
// Name returns the container's name
func (c *Container) Name() string {
return c.name
return c.config.Name
}
// Spec returns the container's OCI runtime spec
// The spec returned is the one used to create the container. The running
// spec may differ slightly as mounts are added based on the image
func (c *Container) Spec() *spec.Spec {
// The spec can potentially be altered when storage is configured and to
// add annotations at container create time
// As such, access to it is locked
c.lock.RLock()
defer c.lock.RUnlock()
spec := new(spec.Spec)
deepcopier.Copy(c.spec).To(spec)
deepcopier.Copy(c.config.Spec).To(spec)
return spec
}
// State returns the current state of the container
func (c *Container) State() (ContainerState, error) {
c.lock.Lock()
defer c.lock.Unlock()
// TODO uncomment when working
// if err := c.runtime.ociRuntime.updateContainerStatus(c); err != nil {
// return ContainerStateUnknown, err
// }
return c.state.State, nil
}
// The path to the container's root filesystem - where the OCI spec will be
// placed, amongst other things
func (c *Container) bundlePath() string {
return c.state.RunDir
}
// Retrieves the path of the container's attach socket
func (c *Container) attachSocketPath() string {
return filepath.Join(c.runtime.ociRuntime.socketsDir, c.ID(), "attach")
}
// Make a new container
func newContainer(rspec *spec.Spec) (*Container, error) {
if rspec == nil {
@ -54,23 +161,199 @@ func newContainer(rspec *spec.Spec) (*Container, error) {
}
ctr := new(Container)
ctr.id = stringid.GenerateNonCryptoID()
ctr.name = ctr.id // TODO generate unique human-readable names
ctr.config = new(containerConfig)
ctr.state = new(containerRuntimeInfo)
ctr.spec = new(spec.Spec)
deepcopier.Copy(rspec).To(ctr.spec)
ctr.config.ID = stringid.GenerateNonCryptoID()
ctr.config.Name = ctr.config.ID // TODO generate unique human-readable names
ctr.config.Spec = new(spec.Spec)
deepcopier.Copy(rspec).To(ctr.config.Spec)
ctr.config.CreatedTime = time.Now()
return ctr, nil
}
// Create container root filesystem for use
func (c *Container) setupStorage() error {
c.lock.Lock()
defer c.lock.Unlock()
if !c.valid {
return errors.Wrapf(ErrCtrRemoved, "container %s is not valid", c.ID())
}
if c.state.State != ContainerStateConfigured {
return errors.Wrapf(ErrCtrStateInvalid, "container %s must be in Configured state to have storage set up", c.ID())
}
// If we're configured to use a directory, perform that setup
if !c.config.RootfsFromImage {
// TODO implement directory-based root filesystems
return ErrNotImplemented
}
// Not using a directory, so call into containers/storage
return c.setupImageRootfs()
}
// Set up an image as root filesystem using containers/storage
func (c *Container) setupImageRootfs() error {
// Need both an image ID and image name, plus a bool telling us whether to use the image configuration
if c.config.RootfsImageID == "" || c.config.RootfsImageName == "" {
return errors.Wrapf(ErrInvalidArg, "must provide image ID and image name to use an image")
}
// TODO SELinux mount label
containerInfo, err := c.runtime.storageService.CreateContainerStorage(c.runtime.imageContext, c.config.RootfsImageName, c.config.RootfsImageID, c.config.Name, c.config.ID, "")
if err != nil {
return errors.Wrapf(err, "error creating container storage")
}
c.config.StaticDir = containerInfo.Dir
c.state.RunDir = containerInfo.RunDir
return nil
}
// Tear down a container's storage prior to removal
func (c *Container) teardownStorage() error {
c.lock.Lock()
defer c.lock.Unlock()
if !c.valid {
return errors.Wrapf(ErrCtrRemoved, "container %s is not valid", c.ID())
}
if c.state.State == ContainerStateRunning || c.state.State == ContainerStatePaused {
return errors.Wrapf(ErrCtrStateInvalid, "cannot remove storage for container %s as it is running or paused", c.ID())
}
if !c.config.RootfsFromImage {
// TODO implement directory-based root filesystems
return ErrNotImplemented
}
return c.teardownImageRootfs()
}
// Completely remove image-based root filesystem for a container
func (c *Container) teardownImageRootfs() error {
if c.state.Mounted {
if err := c.runtime.storageService.StopContainer(c.ID()); err != nil {
return errors.Wrapf(err, "error unmounting container %s root filesystem", c.ID())
}
c.state.Mounted = false
}
if err := c.runtime.storageService.DeleteContainer(c.ID()); err != nil {
return errors.Wrapf(err, "error removing container %s root filesystem", c.ID())
}
return nil
}
// Create creates a container in the OCI runtime
func (c *Container) Create() error {
return ErrNotImplemented
func (c *Container) Create() (err error) {
c.lock.Lock()
defer c.lock.Unlock()
if !c.valid {
return errors.Wrapf(ErrCtrRemoved, "container %s is not valid", c.ID())
}
if c.state.State != ContainerStateConfigured {
return errors.Wrapf(ErrCtrExists, "container %s has already been created in runtime", c.ID())
}
// If using containers/storage, mount the container
if !c.config.RootfsFromImage {
// TODO implement directory-based root filesystems
if !c.state.Mounted {
return ErrNotImplemented
}
} else {
mountPoint, err := c.runtime.storageService.StartContainer(c.ID())
if err != nil {
return errors.Wrapf(err, "error mounting storage for container %s", c.ID())
}
c.state.Mounted = true
c.state.Mountpoint = mountPoint
logrus.Debugf("Created root filesystem for container %s at %s", c.ID(), c.state.Mountpoint)
defer func() {
if err != nil {
if err2 := c.runtime.storageService.StopContainer(c.ID()); err2 != nil {
logrus.Errorf("Error unmounting storage for container %s: %v", c.ID(), err2)
}
c.state.Mounted = false
c.state.Mountpoint = ""
}
}()
}
// Make the OCI runtime spec we will use
c.runningSpec = new(spec.Spec)
deepcopier.Copy(c.config.Spec).To(c.runningSpec)
c.runningSpec.Root.Path = c.state.Mountpoint
c.runningSpec.Annotations[crioAnnotations.Created] = c.config.CreatedTime.Format(time.RFC3339Nano)
// Save the OCI spec to disk
jsonPath := filepath.Join(c.bundlePath(), "config.json")
fileJSON, err := json.Marshal(c.runningSpec)
if err != nil {
return errors.Wrapf(err, "error exporting runtime spec for container %s to JSON", c.ID())
}
if err := ioutil.WriteFile(jsonPath, fileJSON, 0644); err != nil {
return errors.Wrapf(err, "error writing runtime spec JSON to file for container %s", c.ID())
}
c.state.ConfigPath = jsonPath
logrus.Debugf("Created OCI spec for container %s at %s", c.ID(), jsonPath)
// With the spec complete, do an OCI create
// TODO set cgroup parent in a sane fashion
if err := c.runtime.ociRuntime.createContainer(c, "/libpod_parent"); err != nil {
return err
}
logrus.Debugf("Created container %s in runc", c.ID())
// TODO should flush this state to disk here
c.state.State = ContainerStateCreated
return nil
}
// Start starts a container
func (c *Container) Start() error {
return ErrNotImplemented
c.lock.Lock()
defer c.lock.Unlock()
if !c.valid {
return ErrCtrRemoved
}
// Container must be created or stopped to be started
if !(c.state.State == ContainerStateCreated || c.state.State == ContainerStateStopped) {
return errors.Wrapf(ErrCtrStateInvalid, "container %s must be in Created or Stopped state to be started", c.ID())
}
if err := c.runtime.ociRuntime.startContainer(c); err != nil {
return err
}
logrus.Debugf("Started container %s", c.ID())
// TODO should flush state to disk here
c.state.StartedTime = time.Now()
c.state.State = ContainerStateRunning
return nil
}
// Stop stops a container
@ -101,9 +384,13 @@ func (c *Container) Mount() (string, error) {
return "", ErrNotImplemented
}
// Status gets a container's status
// TODO this should return relevant information about container state
func (c *Container) Status() error {
// Pause pauses a container
func (c *Container) Pause() error {
return ErrNotImplemented
}
// Unpause unpauses a container
func (c *Container) Unpause() error {
return ErrNotImplemented
}

View File

@ -20,6 +20,10 @@ var (
// ErrImageExists indicated an image with the same ID already exists
ErrImageExists = errors.New("image already exists")
// ErrCtrStateInvalid indicates a container is in an improper state for
// the requested operation
ErrCtrStateInvalid = errors.New("container state improper")
// ErrRuntimeFinalized indicates that the runtime has already been
// created and cannot be modified
ErrRuntimeFinalized = errors.New("runtime has been finalized")

273
libpod/oci.go Normal file
View File

@ -0,0 +1,273 @@
package libpod
import (
"bytes"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"syscall"
"time"
"github.com/containerd/cgroups"
spec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
// TODO import these functions into libpod and remove the import
// Trying to keep libpod from depending on CRI-O code
"github.com/kubernetes-incubator/cri-o/utils"
)
// OCI code is undergoing heavy rewrite
const (
// CgroupfsCgroupsManager represents cgroupfs native cgroup manager
CgroupfsCgroupsManager = "cgroupfs"
// SystemdCgroupsManager represents systemd native cgroup manager
SystemdCgroupsManager = "systemd"
// ContainerCreateTimeout represents the value of container creating timeout
ContainerCreateTimeout = 240 * time.Second
)
// OCIRuntime represents an OCI-compatible runtime that libpod can call into
// to perform container operations
type OCIRuntime struct {
name string
path string
conmonPath string
conmonEnv []string
cgroupManager string
tmpDir string
exitsDir string
socketsDir string
logSizeMax int64
noPivot bool
}
// syncInfo is used to return data from monitor process to daemon
type syncInfo struct {
Pid int `json:"pid"`
Message string `json:"message,omitempty"`
}
// Make a new OCI runtime with provided options
func newOCIRuntime(name string, path string, conmonPath string, conmonEnv []string, cgroupManager string, tmpDir string, logSizeMax int64, noPivotRoot bool) (*OCIRuntime, error) {
runtime := new(OCIRuntime)
runtime.name = name
runtime.path = path
runtime.conmonPath = conmonPath
runtime.conmonEnv = conmonEnv
runtime.cgroupManager = cgroupManager
runtime.tmpDir = tmpDir
runtime.logSizeMax = logSizeMax
runtime.noPivot = noPivotRoot
runtime.exitsDir = filepath.Join(runtime.tmpDir, "exits")
runtime.socketsDir = filepath.Join(runtime.tmpDir, "socket")
if cgroupManager != CgroupfsCgroupsManager && cgroupManager != SystemdCgroupsManager {
return nil, errors.Wrapf(ErrInvalidArg, "invalid cgroup manager specified: %s", cgroupManager)
}
// Create the exit files and attach sockets directories
if err := os.MkdirAll(runtime.exitsDir, 0750); err != nil {
// The directory is allowed to exist
if !os.IsExist(err) {
return nil, errors.Wrapf(err, "error creating OCI runtime exit files directory %s",
runtime.exitsDir)
}
}
if err := os.MkdirAll(runtime.socketsDir, 0750); err != nil {
// The directory is allowed to exist
if !os.IsExist(err) {
return nil, errors.Wrapf(err, "error creating OCI runtime attach sockets directory %s",
runtime.socketsDir)
}
}
return runtime, nil
}
// newPipe creates a unix socket pair for communication
func newPipe() (parent *os.File, child *os.File, err error) {
fds, err := unix.Socketpair(unix.AF_LOCAL, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
if err != nil {
return nil, nil, err
}
return os.NewFile(uintptr(fds[1]), "parent"), os.NewFile(uintptr(fds[0]), "child"), nil
}
// Create systemd unit name for cgroup scopes
func createUnitName(prefix string, name string) string {
return fmt.Sprintf("%s-%s.scope", prefix, name)
}
// 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) error {
var stderrBuf bytes.Buffer
parentPipe, childPipe, err := newPipe()
if err != nil {
return errors.Wrapf(err, "error creating socket pair")
}
childStartPipe, parentStartPipe, err := newPipe()
if err != nil {
return errors.Wrapf(err, "error creating socket pair for start pipe")
}
defer parentPipe.Close()
defer parentStartPipe.Close()
args := []string{}
if r.cgroupManager == SystemdCgroupsManager {
args = append(args, "-s")
}
args = append(args, "-c", ctr.ID())
args = append(args, "-u", ctr.ID())
args = append(args, "-r", r.path)
args = append(args, "-b", ctr.bundlePath())
args = append(args, "-p", filepath.Join(ctr.state.RunDir, "pidfile"))
// TODO container log location should be configurable
// The default also likely shouldn't be this
args = append(args, "-l", filepath.Join(ctr.config.StaticDir, "ctr.log"))
args = append(args, "--exit-dir", r.exitsDir)
args = append(args, "--socket-dir-path", r.socketsDir)
if ctr.config.Spec.Process.Terminal {
args = append(args, "-t")
} else if ctr.config.Stdin {
args = append(args, "-i")
}
if r.logSizeMax >= 0 {
args = append(args, "--log-size-max", fmt.Sprintf("%v", r.logSizeMax))
}
if r.noPivot {
args = append(args, "--no-pivot")
}
logrus.WithFields(logrus.Fields{
"args": args,
}).Debugf("running conmon: %s", r.conmonPath)
cmd := exec.Command(r.conmonPath, args...)
cmd.Dir = ctr.state.RunDir
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
}
// TODO this is probably a really bad idea for some uses
// Make this configurable
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if ctr.config.Spec.Process.Terminal {
cmd.Stderr = &stderrBuf
}
cmd.ExtraFiles = append(cmd.ExtraFiles, childPipe, childStartPipe)
// 0, 1 and 2 are stdin, stdout and stderr
cmd.Env = append(r.conmonEnv, fmt.Sprintf("_OCI_SYNCPIPE=%d", 3))
cmd.Env = append(cmd.Env, fmt.Sprintf("_OCI_STARTPIPE=%d", 4))
err = cmd.Start()
if err != nil {
childPipe.Close()
return err
}
// We don't need childPipe on the parent side
childPipe.Close()
childStartPipe.Close()
// Move conmon to specified cgroup
if r.cgroupManager == SystemdCgroupsManager {
logrus.Infof("Running conmon under slice %s and unitName %s", cgroupParent, createUnitName("libpod-conmon", ctr.ID()))
if err = utils.RunUnderSystemdScope(cmd.Process.Pid, cgroupParent, createUnitName("libpod-conmon", ctr.ID())); err != nil {
logrus.Warnf("Failed to add conmon to systemd sandbox cgroup: %v", err)
}
} else {
control, err := cgroups.New(cgroups.V1, cgroups.StaticPath(filepath.Join(cgroupParent, "/libpod-conmon-"+ctr.ID())), &spec.LinuxResources{})
if err != nil {
logrus.Warnf("Failed to add conmon to cgroupfs sandbox cgroup: %v", err)
} else {
// XXX: this defer does nothing as the cgroup can't be deleted cause
// it contains the conmon pid in tasks
// we need to remove this defer and delete the cgroup once conmon exits
// maybe need a conmon monitor?
defer control.Delete()
if err := control.Add(cgroups.Process{Pid: cmd.Process.Pid}); err != nil {
logrus.Warnf("Failed to add conmon to cgroupfs sandbox cgroup: %v", err)
}
}
}
/* We set the cgroup, now the child can start creating children */
someData := []byte{0}
_, err = parentStartPipe.Write(someData)
if err != nil {
return err
}
/* Wait for initial setup and fork, and reap child */
err = cmd.Wait()
if err != nil {
return err
}
// TODO should do a defer r.deleteContainer(ctr) here if err != nil
// Need deleteContainer to be working first, though...
// Wait to get container pid from conmon
type syncStruct struct {
si *syncInfo
err error
}
ch := make(chan syncStruct)
go func() {
var si *syncInfo
if err = json.NewDecoder(parentPipe).Decode(&si); err != nil {
ch <- syncStruct{err: err}
return
}
ch <- syncStruct{si: si}
}()
select {
case ss := <-ch:
if ss.err != nil {
return errors.Wrapf(ss.err, "error reading container (probably exited) json message")
}
logrus.Debugf("Received container pid: %d", ss.si.Pid)
if ss.si.Pid == -1 {
if ss.si.Message != "" {
return errors.Wrapf(ErrInternal, "container create failed: %s", ss.si.Message)
}
return errors.Wrapf(ErrInternal, "container create failed")
}
case <-time.After(ContainerCreateTimeout):
return errors.Wrapf(ErrInternal, "container creation timeout")
}
return nil
}
// updateContainerStatus retrieves the current status of the container from the
// runtime
func (r *OCIRuntime) updateContainerStatus(ctr *Container) error {
return ErrNotImplemented
}
// startContainer starts the given container
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, r.path, "start", ctr.ID()); err != nil {
return err
}
// TODO record start time in container struct
return nil
}

View File

@ -137,6 +137,7 @@ func WithConmonEnv(environment []string) RuntimeOption {
// WithCgroupManager specifies the manager implementation name which is used to
// handle cgroups for containers
// Current valid values are "cgroupfs" and "systemd"
func WithCgroupManager(manager string) RuntimeOption {
return func(rt *Runtime) error {
if rt.valid {
@ -149,6 +150,35 @@ func WithCgroupManager(manager string) RuntimeOption {
}
}
// WithStaticDir sets the directory that static runtime files which persist
// across reboots will be stored
func WithStaticDir(dir string) RuntimeOption {
return func(rt *Runtime) error {
if rt.valid {
return ErrRuntimeFinalized
}
rt.config.StaticDir = dir
return nil
}
}
// WithTmpDir sets the directory that temporary runtime files which are not
// expected to survive across reboots will be stored
// This should be located on a tmpfs mount (/tmp or /var/run for example)
func WithTmpDir(dir string) RuntimeOption {
return func(rt *Runtime) error {
if rt.valid {
return ErrRuntimeFinalized
}
rt.config.TmpDir = dir
return nil
}
}
// WithSELinux enables SELinux on the container server
func WithSELinux() RuntimeOption {
return func(rt *Runtime) error {
@ -176,19 +206,89 @@ func WithPidsLimit(limit int64) RuntimeOption {
}
}
// WithMaxLogSize sets the maximum size of container logs
// Positive sizes are limits in bytes, -1 is unlimited
func WithMaxLogSize(limit int64) RuntimeOption {
return func(rt *Runtime) error {
if rt.valid {
return ErrRuntimeFinalized
}
rt.config.MaxLogSize = limit
return nil
}
}
// WithNoPivotRoot sets the runtime to use MS_MOVE instead of PIVOT_ROOT when
// starting containers
func WithNoPivotRoot(noPivot bool) RuntimeOption {
return func(rt *Runtime) error {
if rt.valid {
return ErrRuntimeFinalized
}
rt.config.NoPivotRoot = true
return nil
}
}
// Container Creation Options
// WithRootFSFromPath uses the given path as a container's root filesystem
// No further setup is performed on this path
func WithRootFSFromPath(path string) CtrCreateOption {
return ctrNotImplemented
return func(ctr *Container) error {
if ctr.valid {
return ErrCtrFinalized
}
if ctr.config.RootfsDir != "" || ctr.config.RootfsImageID != "" || ctr.config.RootfsImageName != "" {
return errors.Wrapf(ErrInvalidArg, "container already configured with root filesystem")
}
ctr.config.RootfsDir = path
ctr.config.RootfsFromImage = false
return nil
}
}
// WithRootFSFromImage sets up a fresh root filesystem using the given image
// If useImageConfig is specified, image volumes, environment variables, and
// other configuration from the image will be added to the config
func WithRootFSFromImage(image string, useImageConfig bool) CtrCreateOption {
return ctrNotImplemented
// TODO: Replace image name and ID with a libpod.Image struct when that is finished
func WithRootFSFromImage(imageID string, imageName string, useImageConfig bool) CtrCreateOption {
return func(ctr *Container) error {
if ctr.valid {
return ErrCtrFinalized
}
if ctr.config.RootfsDir != "" || ctr.config.RootfsImageID != "" || ctr.config.RootfsImageName != "" {
return errors.Wrapf(ErrInvalidArg, "container already configured with root filesystem")
}
ctr.config.RootfsImageID = imageID
ctr.config.RootfsImageName = imageName
ctr.config.UseImageConfig = useImageConfig
ctr.config.RootfsFromImage = true
return nil
}
}
// WithStdin keeps stdin on the container open to allow interaction
func WithStdin() CtrCreateOption {
return func(ctr *Container) error {
if ctr.valid {
return ErrCtrFinalized
}
ctr.config.Stdin = true
return nil
}
}
// WithSharedNamespaces sets a container to share namespaces with another
@ -203,25 +303,15 @@ func WithSharedNamespaces(from *Container, namespaces map[string]string) CtrCrea
// WithPod adds the container to a pod
func (r *Runtime) WithPod(pod *Pod) CtrCreateOption {
return func(ctr *Container) error {
if !ctr.valid {
if ctr.valid {
return ErrCtrFinalized
}
if ctr.pod != nil {
return fmt.Errorf("container has already been added to a pod")
}
exists, err := r.state.HasPod(pod.ID())
if err != nil {
return errors.Wrapf(err, "error searching state for pod %s", pod.ID())
} else if !exists {
return errors.Wrapf(ErrNoSuchPod, "pod %s cannot be found in state", pod.ID())
}
if err := pod.addContainer(ctr); err != nil {
return errors.Wrapf(err, "error adding container to pod")
if pod == nil {
return ErrInvalidArg
}
ctr.config.Pod = pod.ID()
ctr.pod = pod
return nil
@ -241,11 +331,11 @@ func WithAnnotations(annotations map[string]string) CtrCreateOption {
// WithName sets the container's name
func WithName(name string) CtrCreateOption {
return func(ctr *Container) error {
if !ctr.valid {
if ctr.valid {
return ErrCtrFinalized
}
ctr.name = name
ctr.config.Name = name
return nil
}

View File

@ -45,16 +45,22 @@ func newPod() (*Pod, error) {
func (p *Pod) addContainer(ctr *Container) error {
p.lock.Lock()
defer p.lock.Unlock()
ctr.lock.Lock()
defer ctr.lock.Unlock()
if !p.valid {
return ErrPodRemoved
}
if _, ok := p.containers[ctr.id]; ok {
return errors.Wrapf(ErrCtrExists, "container with ID %s already exists in pod %s", ctr.id, p.id)
if !ctr.valid {
return ErrCtrRemoved
}
p.containers[ctr.id] = ctr
if _, ok := p.containers[ctr.ID()]; ok {
return errors.Wrapf(ErrCtrExists, "container with ID %s already exists in pod %s", ctr.ID(), p.id)
}
p.containers[ctr.ID()] = ctr
return nil
}
@ -69,11 +75,11 @@ func (p *Pod) removeContainer(ctr *Container) error {
return ErrPodRemoved
}
if _, ok := p.containers[ctr.id]; !ok {
return errors.Wrapf(ErrNoSuchCtr, "no container with id %s in pod %s", ctr.id, p.id)
if _, ok := p.containers[ctr.ID()]; !ok {
return errors.Wrapf(ErrNoSuchCtr, "no container with id %s in pod %s", ctr.ID(), p.id)
}
delete(p.containers, ctr.id)
delete(p.containers, ctr.ID())
return nil
}

View File

@ -1,13 +1,12 @@
package libpod
import (
"os"
"sync"
is "github.com/containers/image/storage"
"github.com/containers/image/types"
"github.com/containers/storage"
"github.com/kubernetes-incubator/cri-o/server/apparmor"
"github.com/kubernetes-incubator/cri-o/server/seccomp"
"github.com/pkg/errors"
"github.com/ulule/deepcopier"
)
@ -18,14 +17,14 @@ type RuntimeOption func(*Runtime) error
// Runtime is the core libpod runtime
type Runtime struct {
config *RuntimeConfig
state State
store storage.Store
imageContext *types.SystemContext
apparmorEnabled bool
seccompEnabled bool
valid bool
lock sync.RWMutex
config *RuntimeConfig
state State
store storage.Store
storageService *storageService
imageContext *types.SystemContext
ociRuntime *OCIRuntime
valid bool
lock sync.RWMutex
}
// RuntimeConfig contains configuration options used to set up the runtime
@ -39,8 +38,12 @@ type RuntimeConfig struct {
ConmonPath string
ConmonEnvVars []string
CgroupManager string
StaticDir string
TmpDir string
SelinuxEnabled bool
PidsLimit int64
MaxLogSize int64
NoPivotRoot bool
}
var (
@ -54,8 +57,12 @@ var (
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
},
CgroupManager: "cgroupfs",
StaticDir: "/var/lib/libpod",
TmpDir: "/var/run/libpod",
SelinuxEnabled: false,
PidsLimit: 1024,
MaxLogSize: -1,
NoPivotRoot: false,
}
)
@ -83,6 +90,14 @@ func NewRuntime(options ...RuntimeOption) (*Runtime, error) {
runtime.store = store
is.Transport.SetStore(store)
// TODO remove StorageImageServer and make its functions work directly
// on Runtime (or convert to something that satisfies an image)
storageService, err := getStorageService(runtime.store)
if err != nil {
return nil, err
}
runtime.storageService = storageService
// Set up containers/image
runtime.imageContext = &types.SystemContext{
SignaturePolicyPath: runtime.config.SignaturePolicyPath,
@ -95,8 +110,33 @@ func NewRuntime(options ...RuntimeOption) (*Runtime, error) {
}
runtime.state = state
runtime.seccompEnabled = seccomp.IsEnabled()
runtime.apparmorEnabled = apparmor.IsEnabled()
// Make an OCI runtime to perform container operations
ociRuntime, err := newOCIRuntime("runc", runtime.config.RuntimePath,
runtime.config.ConmonPath, runtime.config.ConmonEnvVars,
runtime.config.CgroupManager, runtime.config.TmpDir,
runtime.config.MaxLogSize, runtime.config.NoPivotRoot)
if err != nil {
return nil, err
}
runtime.ociRuntime = ociRuntime
// Make the static files directory if it does not exist
if err := os.MkdirAll(runtime.config.StaticDir, 0755); err != nil {
// The directory is allowed to exist
if !os.IsExist(err) {
return nil, errors.Wrapf(err, "error creating runtime static files directory %s",
runtime.config.StaticDir)
}
}
// Make the per-boot files directory if it does not exist
if err := os.MkdirAll(runtime.config.TmpDir, 0755); err != nil {
// The directory is allowed to exist
if !os.IsExist(err) {
return nil, errors.Wrapf(err, "error creating runtime temporary files directory %s",
runtime.config.TmpDir)
}
}
// Mark the runtime as valid - ready to be used, cannot be modified
// further

View File

@ -4,6 +4,7 @@ import (
"github.com/containers/storage"
spec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
// Contains the public Runtime API for containers
@ -18,7 +19,7 @@ type CtrCreateOption func(*Container) error
type ContainerFilter func(*Container) bool
// NewContainer creates a new container from a given OCI config
func (r *Runtime) NewContainer(spec *spec.Spec, options ...CtrCreateOption) (*Container, error) {
func (r *Runtime) NewContainer(spec *spec.Spec, options ...CtrCreateOption) (ctr *Container, err error) {
r.lock.Lock()
defer r.lock.Unlock()
@ -26,7 +27,7 @@ func (r *Runtime) NewContainer(spec *spec.Spec, options ...CtrCreateOption) (*Co
return nil, ErrRuntimeStopped
}
ctr, err := newContainer(spec)
ctr, err = newContainer(spec)
if err != nil {
return nil, err
}
@ -38,15 +39,36 @@ func (r *Runtime) NewContainer(spec *spec.Spec, options ...CtrCreateOption) (*Co
}
ctr.valid = true
ctr.state.State = ContainerStateConfigured
ctr.runtime = r
if err := r.state.AddContainer(ctr); err != nil {
// If we joined a pod, remove ourself from it
if ctr.pod != nil {
if err2 := ctr.pod.removeContainer(ctr); err2 != nil {
return nil, errors.Wrapf(err, "error adding new container to state, container could not be removed from pod %s", ctr.pod.ID())
// Set up storage for the container
if err := ctr.setupStorage(); err != nil {
return nil, errors.Wrapf(err, "error configuring storage for container")
}
defer func() {
if err != nil {
if err2 := ctr.teardownStorage(); err2 != nil {
logrus.Errorf("Error removing partially-created container root filesystem: %s", err2)
}
}
}()
// If the container is in a pod, add it to the pod
if ctr.pod != nil {
if err := ctr.pod.addContainer(ctr); err != nil {
return nil, errors.Wrapf(err, "error adding new container to pod %s", ctr.pod.ID())
}
}
defer func() {
if err != nil {
if err2 := ctr.pod.removeContainer(ctr); err2 != nil {
logrus.Errorf("Error removing partially-created container from pod %s: %s", ctr.pod.ID(), err2)
}
}
}()
if err := r.state.AddContainer(ctr); err != nil {
// TODO: Might be worth making an effort to detect duplicate IDs
// We can recover from that by generating a new ID for the
// container
@ -77,10 +99,16 @@ func (r *Runtime) RemoveContainer(c *Container, force bool) error {
// TODO check container status and unmount storage
// TODO check that no other containers depend on this container's
// namespaces
if err := c.Status(); err != nil {
status, err := c.State()
if err != nil {
return err
}
// A container cannot be removed if it is running
if status == ContainerStateRunning {
return errors.Wrapf(ErrCtrStateInvalid, "cannot remove container %s as it is running", c.ID())
}
if err := r.state.RemoveContainer(c); err != nil {
return errors.Wrapf(err, "error removing container from state")
}
@ -185,6 +213,7 @@ func (r *Runtime) getContainersWithImage(imageID string) ([]storage.Container, e
}
// removeMultipleContainers deletes a list of containers from the store
// TODO refactor this to remove libpod Containers
func (r *Runtime) removeMultipleContainers(containers []storage.Container) error {
for _, ctr := range containers {
if err := r.store.DeleteContainer(ctr.ID); err != nil {
@ -193,3 +222,8 @@ func (r *Runtime) removeMultipleContainers(containers []storage.Container) error
}
return nil
}
// ContainerConfigToDisk saves a container's nonvolatile configuration to disk
func (r *Runtime) containerConfigToDisk(ctr *Container) error {
return ErrNotImplemented
}

261
libpod/storage.go Normal file
View File

@ -0,0 +1,261 @@
package libpod
import (
"encoding/json"
"time"
istorage "github.com/containers/image/storage"
"github.com/containers/image/types"
"github.com/containers/storage"
"github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
type storageService struct {
store storage.Store
}
// getStorageService returns a storageService which can create container root
// filesystems from images
func getStorageService(store storage.Store) (*storageService, error) {
return &storageService{store: store}, nil
}
// ContainerInfo wraps a subset of information about a container: the locations
// of its nonvolatile and volatile per-container directories, along with a copy
// of the configuration blob from the image that was used to create the
// container, if the image had a configuration.
type ContainerInfo struct {
Dir string
RunDir string
Config *v1.Image
}
// RuntimeContainerMetadata is the structure that we encode as JSON and store
// in the metadata field of storage.Container objects. It is used for
// specifying attributes containers when they are being created, and allows a
// container's MountLabel, and possibly other values, to be modified in one
// read/write cycle via calls to storageService.ContainerMetadata,
// RuntimeContainerMetadata.SetMountLabel, and
// storageService.SetContainerMetadata.
type RuntimeContainerMetadata struct {
// The provided name and the ID of the image that was used to
// instantiate the container.
ImageName string `json:"image-name"` // Applicable to both PodSandboxes and Containers
ImageID string `json:"image-id"` // Applicable to both PodSandboxes and Containers
// The container's name, which for an infrastructure container is usually PodName + "-infra".
ContainerName string `json:"name"` // Applicable to both PodSandboxes and Containers, mandatory
CreatedAt int64 `json:"created-at"` // Applicable to both PodSandboxes and Containers
MountLabel string `json:"mountlabel,omitempty"` // Applicable to both PodSandboxes and Containers
}
// SetMountLabel updates the mount label held by a RuntimeContainerMetadata
// object.
func (metadata *RuntimeContainerMetadata) SetMountLabel(mountLabel string) {
metadata.MountLabel = mountLabel
}
// CreateContainerStorage creates the storage end of things. We already have the container spec created
// TO-DO We should be passing in an KpodImage object in the future.
func (r *storageService) CreateContainerStorage(systemContext *types.SystemContext, imageName, imageID, containerName, containerID, mountLabel string) (ContainerInfo, error) {
var ref types.ImageReference
if imageName == "" && imageID == "" {
return ContainerInfo{}, ErrEmptyID
}
if containerName == "" {
return ContainerInfo{}, ErrEmptyID
}
//// Check if we have the specified image.
ref, err := istorage.Transport.ParseStoreReference(r.store, imageName)
if err != nil {
return ContainerInfo{}, err
}
img, err := istorage.Transport.GetStoreImage(r.store, ref)
if err != nil {
return ContainerInfo{}, err
}
// Pull out a copy of the image's configuration.
image, err := ref.NewImage(systemContext)
if err != nil {
return ContainerInfo{}, err
}
defer image.Close()
imageConfig, err := image.OCIConfig()
if err != nil {
return ContainerInfo{}, err
}
// Update the image name and ID.
if imageName == "" && len(img.Names) > 0 {
imageName = img.Names[0]
}
imageID = img.ID
// Build metadata to store with the container.
metadata := RuntimeContainerMetadata{
ImageName: imageName,
ImageID: imageID,
ContainerName: containerName,
CreatedAt: time.Now().Unix(),
MountLabel: mountLabel,
}
mdata, err := json.Marshal(&metadata)
if err != nil {
return ContainerInfo{}, err
}
// Build the container.
names := []string{containerName}
container, err := r.store.CreateContainer(containerID, names, img.ID, "", string(mdata), nil)
if err != nil {
logrus.Debugf("failed to create container %s(%s): %v", metadata.ContainerName, containerID, err)
return ContainerInfo{}, err
}
logrus.Debugf("created container %q", container.ID)
// If anything fails after this point, we need to delete the incomplete
// container before returning.
defer func() {
if err != nil {
if err2 := r.store.DeleteContainer(container.ID); err2 != nil {
logrus.Infof("%v deleting partially-created container %q", err2, container.ID)
return
}
logrus.Infof("deleted partially-created container %q", container.ID)
}
}()
// Add a name to the container's layer so that it's easier to follow
// what's going on if we're just looking at the storage-eye view of things.
layerName := metadata.ContainerName + "-layer"
names, err = r.store.Names(container.LayerID)
if err != nil {
return ContainerInfo{}, err
}
names = append(names, layerName)
err = r.store.SetNames(container.LayerID, names)
if err != nil {
return ContainerInfo{}, err
}
// Find out where the container work directories are, so that we can return them.
containerDir, err := r.store.ContainerDirectory(container.ID)
if err != nil {
return ContainerInfo{}, err
}
logrus.Debugf("container %q has work directory %q", container.ID, containerDir)
containerRunDir, err := r.store.ContainerRunDirectory(container.ID)
if err != nil {
return ContainerInfo{}, err
}
logrus.Debugf("container %q has run directory %q", container.ID, containerRunDir)
return ContainerInfo{
Dir: containerDir,
RunDir: containerRunDir,
Config: imageConfig,
}, nil
}
func (r *storageService) DeleteContainer(idOrName string) error {
if idOrName == "" {
return ErrEmptyID
}
container, err := r.store.Container(idOrName)
if err != nil {
return err
}
err = r.store.DeleteContainer(container.ID)
if err != nil {
logrus.Debugf("failed to delete container %q: %v", container.ID, err)
return err
}
return nil
}
func (r *storageService) SetContainerMetadata(idOrName string, metadata RuntimeContainerMetadata) error {
mdata, err := json.Marshal(&metadata)
if err != nil {
logrus.Debugf("failed to encode metadata for %q: %v", idOrName, err)
return err
}
return r.store.SetMetadata(idOrName, string(mdata))
}
func (r *storageService) GetContainerMetadata(idOrName string) (RuntimeContainerMetadata, error) {
metadata := RuntimeContainerMetadata{}
mdata, err := r.store.Metadata(idOrName)
if err != nil {
return metadata, err
}
if err = json.Unmarshal([]byte(mdata), &metadata); err != nil {
return metadata, err
}
return metadata, nil
}
func (r *storageService) StartContainer(idOrName string) (string, error) {
container, err := r.store.Container(idOrName)
if err != nil {
if errors.Cause(err) == storage.ErrContainerUnknown {
return "", ErrNoSuchCtr
}
return "", err
}
metadata := RuntimeContainerMetadata{}
if err = json.Unmarshal([]byte(container.Metadata), &metadata); err != nil {
return "", err
}
mountPoint, err := r.store.Mount(container.ID, metadata.MountLabel)
if err != nil {
logrus.Debugf("failed to mount container %q: %v", container.ID, err)
return "", err
}
logrus.Debugf("mounted container %q at %q", container.ID, mountPoint)
return mountPoint, nil
}
func (r *storageService) StopContainer(idOrName string) error {
if idOrName == "" {
return ErrEmptyID
}
container, err := r.store.Container(idOrName)
if err != nil {
return err
}
err = r.store.Unmount(container.ID)
if err != nil {
logrus.Debugf("failed to unmount container %q: %v", container.ID, err)
return err
}
logrus.Debugf("unmounted container %q", container.ID)
return nil
}
func (r *storageService) GetWorkDir(id string) (string, error) {
container, err := r.store.Container(id)
if err != nil {
if errors.Cause(err) == storage.ErrContainerUnknown {
return "", ErrNoSuchCtr
}
return "", err
}
return r.store.ContainerDirectory(container.ID)
}
func (r *storageService) GetRunDir(id string) (string, error) {
container, err := r.store.Container(id)
if err != nil {
if errors.Cause(err) == storage.ErrContainerUnknown {
return "", ErrNoSuchCtr
}
return "", err
}
return r.store.ContainerRunDirectory(container.ID)
}