diff options
author | Nicolas Lacasse <nlacasse@google.com> | 2018-05-15 10:17:19 -0700 |
---|---|---|
committer | Shentubot <shentubot@google.com> | 2018-05-15 10:18:03 -0700 |
commit | 205f1027e6beb84101439172b3c776c2671b5be8 (patch) | |
tree | 10294e667ee529e140c474c475e7309cb72ea1d8 /runsc/container/container.go | |
parent | ed02ac4f668ec41063cd51cbbd451baba9e9a6e7 (diff) |
Refactor the Sandbox package into Sandbox + Container.
This is a necessary prerequisite for supporting multiple containers in a single
sandbox.
All the commands (in cmd package) now call operations on Containers (container
package). When a Container first starts, it will create a Sandbox with the same
ID.
The Sandbox class is now simpler, as it only knows how to create boot/gofer
processes, and how to forward commands into the running boot process.
There are TODOs sprinkled around for additional support for multiple
containers. Most notably, we need to detect when a container is intended to run
in an existing sandbox (by reading the metadata), and then have some way to
signal to the sandbox to start a new container. Other urpc calls into the
sandbox need to pass the container ID, so the sandbox can run the operation on
the given container. These are only half-plummed through right now.
PiperOrigin-RevId: 196688269
Change-Id: I1ecf4abbb9dd8987a53ae509df19341aaf42b5b0
Diffstat (limited to 'runsc/container/container.go')
-rw-r--r-- | runsc/container/container.go | 380 |
1 files changed, 380 insertions, 0 deletions
diff --git a/runsc/container/container.go b/runsc/container/container.go new file mode 100644 index 000000000..97115cd6b --- /dev/null +++ b/runsc/container/container.go @@ -0,0 +1,380 @@ +// Copyright 2018 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package container creates and manipulates containers. +package container + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "regexp" + "strconv" + "syscall" + "time" + + specs "github.com/opencontainers/runtime-spec/specs-go" + "gvisor.googlesource.com/gvisor/pkg/log" + "gvisor.googlesource.com/gvisor/pkg/sentry/control" + "gvisor.googlesource.com/gvisor/runsc/boot" + "gvisor.googlesource.com/gvisor/runsc/sandbox" + "gvisor.googlesource.com/gvisor/runsc/specutils" +) + +// metadataFilename is the name of the metadata file relative to the container +// root directory that holds sandbox metadata. +const metadataFilename = "meta.json" + +// validateID validates the container id. +func validateID(id string) error { + // See libcontainer/factory_linux.go. + idRegex := regexp.MustCompile(`^[\w+-\.]+$`) + if !idRegex.MatchString(id) { + return fmt.Errorf("invalid container id: %v", id) + } + return nil +} + +// Container represents a containerized application. When running, the +// container is associated with a single Sandbox. +// +// Container metadata can be saved and loaded to disk. Within a root directory, +// we maintain subdirectories for each container named with the container id. +// The container metadata is is stored as json within the container directory +// in a file named "meta.json". This metadata format is defined by us, and is +// not part of the OCI spec. +// +// Containers must write their metadata file after any change to their internal +// state. The entire container directory is deleted when the container is +// destroyed. +type Container struct { + // ID is the container ID. + ID string `json:"id"` + + // Spec is the OCI runtime spec that configures this container. + Spec *specs.Spec `json:"spec"` + + // BundleDir is the directory containing the container bundle. + BundleDir string `json:"bundleDir"` + + // Root is the directory containing the container metadata file. + Root string `json:"root"` + + // CreatedAt is the time the container was created. + CreatedAt time.Time `json:"createdAt"` + + // Owner is the container owner. + Owner string `json:"owner"` + + // ConsoleSocket is the path to a unix domain socket that will receive + // the console FD. It is only used during create, so we don't need to + // store it in the metadata. + ConsoleSocket string `json:"-"` + + // Status is the current container Status. + Status Status `json:"status"` + + // Sandbox is the sandbox this container is running in. It will be nil + // if the container is not in state Running or Created. + Sandbox *sandbox.Sandbox `json:"sandbox"` +} + +// Load loads a container with the given id from a metadata file. +func Load(rootDir, id string) (*Container, error) { + log.Debugf("Load container %q %q", rootDir, id) + if err := validateID(id); err != nil { + return nil, err + } + cRoot := filepath.Join(rootDir, id) + if !exists(cRoot) { + return nil, fmt.Errorf("container with id %q does not exist", id) + } + metaFile := filepath.Join(cRoot, metadataFilename) + if !exists(metaFile) { + return nil, fmt.Errorf("container with id %q does not have metadata file %q", id, metaFile) + } + metaBytes, err := ioutil.ReadFile(metaFile) + if err != nil { + return nil, fmt.Errorf("error reading container metadata file %q: %v", metaFile, err) + } + var c Container + if err := json.Unmarshal(metaBytes, &c); err != nil { + return nil, fmt.Errorf("error unmarshaling container metadata from %q: %v", metaFile, err) + } + + // If the status is "Running" or "Created", check that the sandbox + // process still exists, and set it to Stopped if it does not. + // + // This is inherently racey. + if c.Status == Running || c.Status == Created { + // Send signal 0 to check if container still exists. + if err := c.Signal(0); err != nil { + // Container no longer exists. + c.Status = Stopped + c.Sandbox = nil + } + } + + return &c, nil +} + +// List returns all container ids in the given root directory. +func List(rootDir string) ([]string, error) { + log.Debugf("List containers %q", rootDir) + fs, err := ioutil.ReadDir(rootDir) + if err != nil { + return nil, fmt.Errorf("ReadDir(%s) failed: %v", rootDir, err) + } + var out []string + for _, f := range fs { + out = append(out, f.Name()) + } + return out, nil +} + +// Create creates the container in a new Sandbox process, unless the metadata +// indicates that an existing Sandbox should be used. +func Create(id string, spec *specs.Spec, conf *boot.Config, bundleDir, consoleSocket, pidFile string) (*Container, error) { + log.Debugf("Create container %q in root dir: %s", id, conf.RootDir) + if err := validateID(id); err != nil { + return nil, err + } + if err := specutils.ValidateSpec(spec); err != nil { + return nil, err + } + + containerRoot := filepath.Join(conf.RootDir, id) + if exists(containerRoot) { + return nil, fmt.Errorf("container with id %q already exists: %q ", id, containerRoot) + } + + c := &Container{ + ID: id, + Spec: spec, + ConsoleSocket: consoleSocket, + BundleDir: bundleDir, + Root: containerRoot, + Status: Creating, + Owner: os.Getenv("USER"), + } + + // TODO: If the metadata annotations indicates that this + // container should be started in another sandbox, we must do so. The + // metadata will indicate the ID of the sandbox, which is the same as + // the ID of the init container in the sandbox. We can look up that + // init container by ID to get the sandbox, then we need to expose a + // way to run a new container in the sandbox. + + // Start a new sandbox for this container. Any errors after this point + // must destroy the container. + s, err := sandbox.Create(id, spec, conf, bundleDir, consoleSocket) + if err != nil { + c.Destroy() + return nil, err + } + + c.Sandbox = s + c.Status = Created + + // Save the metadata file. + if err := c.save(); err != nil { + c.Destroy() + return nil, err + } + + // Write the pid file. Containerd considers the create complete after + // this file is created, so it must be the last thing we do. + if pidFile != "" { + if err := ioutil.WriteFile(pidFile, []byte(strconv.Itoa(c.Pid())), 0644); err != nil { + s.Destroy() + return nil, fmt.Errorf("error writing pid file: %v", err) + } + } + + return c, nil +} + +// Start starts running the containerized process inside the sandbox. +func (c *Container) Start(conf *boot.Config) error { + log.Debugf("Start container %q", c.ID) + if c.Status != Created { + return fmt.Errorf("cannot start container in state %s", c.Status) + } + + // "If any prestart hook fails, the runtime MUST generate an error, + // stop and destroy the container". + if c.Spec.Hooks != nil { + if err := executeHooks(c.Spec.Hooks.Prestart, c.State()); err != nil { + c.Destroy() + return err + } + } + + if err := c.Sandbox.Start(c.ID, c.Spec, conf); err != nil { + c.Destroy() + return err + } + + // "If any poststart hook fails, the runtime MUST log a warning, but + // the remaining hooks and lifecycle continue as if the hook had + // succeeded". + if c.Spec.Hooks != nil { + executeHooksBestEffort(c.Spec.Hooks.Poststart, c.State()) + } + + c.Status = Running + return c.save() +} + +// Run is a helper that calls Create + Start + Wait. +func Run(id string, spec *specs.Spec, conf *boot.Config, bundleDir, consoleSocket, pidFile string) (syscall.WaitStatus, error) { + log.Debugf("Run container %q in root dir: %s", id, conf.RootDir) + c, err := Create(id, spec, conf, bundleDir, consoleSocket, pidFile) + if err != nil { + return 0, fmt.Errorf("error creating container: %v", err) + } + if err := c.Start(conf); err != nil { + return 0, fmt.Errorf("error starting container: %v", err) + } + return c.Wait() +} + +// Execute runs the specified command in the container. +func (c *Container) Execute(e *control.ExecArgs) (syscall.WaitStatus, error) { + log.Debugf("Execute in container %q, args: %+v", c.ID, e) + if c.Status != Created && c.Status != Running { + return 0, fmt.Errorf("cannot exec in container in state %s", c.Status) + } + return c.Sandbox.Execute(c.ID, e) +} + +// Event returns events for the container. +func (c *Container) Event() (*boot.Event, error) { + log.Debugf("Getting events for container %q", c.ID) + if c.Status != Running && c.Status != Created { + return nil, fmt.Errorf("cannot get events for container in state: %s", c.Status) + } + return c.Sandbox.Event(c.ID) +} + +// Pid returns the Pid of the sandbox the container is running in, or -1 if the +// container is not running. +func (c *Container) Pid() int { + if c.Status != Running && c.Status != Created { + return -1 + } + return c.Sandbox.Pid +} + +// Wait waits for the container to exit, and returns its WaitStatus. +func (c *Container) Wait() (syscall.WaitStatus, error) { + log.Debugf("Wait on container %q", c.ID) + return c.Sandbox.Wait(c.ID) +} + +// Signal sends the signal to the container. +func (c *Container) Signal(sig syscall.Signal) error { + log.Debugf("Signal container %q", c.ID) + if c.Status == Stopped { + log.Warningf("container %q not running, not sending signal %v", c.ID, sig) + return nil + } + return c.Sandbox.Signal(c.ID, sig) +} + +// State returns the metadata of the container. +func (c *Container) State() specs.State { + return specs.State{ + Version: specs.Version, + ID: c.ID, + Status: c.Status.String(), + Pid: c.Pid(), + Bundle: c.BundleDir, + } +} + +// Processes retrieves the list of processes and associated metadata inside a +// container. +func (c *Container) Processes() ([]*control.Process, error) { + if c.Status != Running { + return nil, fmt.Errorf("cannot get processes of container %q because it isn't running. It is in state %v", c.ID, c.Status) + } + return c.Sandbox.Processes(c.ID) +} + +// Destroy frees all resources associated with the container. +func (c *Container) Destroy() error { + log.Debugf("Destroy container %q", c.ID) + + // First stop the container. + if err := c.Sandbox.Stop(c.ID); err != nil { + return err + } + + // Then destroy all the metadata. + if err := os.RemoveAll(c.Root); err != nil { + log.Warningf("Failed to delete container root directory %q, err: %v", c.Root, err) + } + + // "If any poststop hook fails, the runtime MUST log a warning, but the + // remaining hooks and lifecycle continue as if the hook had succeeded". + if c.Spec.Hooks != nil && (c.Status == Created || c.Status == Running) { + executeHooksBestEffort(c.Spec.Hooks.Poststop, c.State()) + } + + if err := os.RemoveAll(c.Root); err != nil { + log.Warningf("Failed to delete container root directory %q, err: %v", c.Root, err) + } + + // If we are the first container in the sandbox, take the sandbox down + // as well. + if c.Sandbox != nil && c.Sandbox.ID == c.ID { + if err := c.Sandbox.Destroy(); err != nil { + log.Warningf("Failed to destroy sandbox %q: %v", c.Sandbox.ID, err) + } + } + + c.Sandbox = nil + c.Status = Stopped + return nil +} + +// save saves the container metadata to a file. +func (c *Container) save() error { + log.Debugf("Save container %q", c.ID) + if err := os.MkdirAll(c.Root, 0711); err != nil { + return fmt.Errorf("error creating container root directory %q: %v", c.Root, err) + } + meta, err := json.Marshal(c) + if err != nil { + return fmt.Errorf("error marshaling container metadata: %v", err) + } + metaFile := filepath.Join(c.Root, metadataFilename) + if err := ioutil.WriteFile(metaFile, meta, 0640); err != nil { + return fmt.Errorf("error writing container metadata: %v", err) + } + return nil +} + +// exists returns true if the given file exists. +func exists(f string) bool { + if _, err := os.Stat(f); err == nil { + return true + } else if !os.IsNotExist(err) { + log.Warningf("error checking for file %q: %v", f, err) + } + return false +} |