Add storage utility functions

Add an intermediate API layer that uses containers/storage, and a
containers/image that has been patched to use it, to manage images and
containers, storing the data that we need to know about containers and
pods in the metadata fields provided by containers/storage.

While ocid manages pods and containers as different types of items, with
disjoint sets of IDs and names, it remains true that every pod includes
at least one container.  When a container's only purpose is to serve as
a home for namespaces that are shared with the other containers in the
pod, it is referred to as the pod's infrastructure container.

At the storage level, a pod is stored as its set of containers.  We keep
track of both pod IDs and container IDs in the metadata field of
Container objects that the storage library manages for us.  Containers
which bear the same pod ID are members of the pod which has that ID.
Other information about the pod, which ocid needs to remember in order
to answer requests for information about the pod, is also kept in the
metadata field of its member containers.

The container's runtime configuration should be stored in the
container's ContainerDirectory, and used as a template.  Each time the
container is about to be started, its layer should be mounted, that
configuration template should be read, the template's rootfs location
should be replaced with the mountpoint for the container's layer, and
the result should be saved to the container's ContainerRunDirectory,
for use as the configuration for the container.

Signed-off-by: Nalin Dahyabhai <nalin@redhat.com>
This commit is contained in:
Nalin Dahyabhai 2016-11-22 13:50:33 -05:00
parent d45ff58056
commit 4ae8606edf
3 changed files with 656 additions and 0 deletions

5
pkg/storage/doc.go Normal file
View file

@ -0,0 +1,5 @@
// Package storage provides helper functions for creating and managing CRI pod
// sandboxes and containers and metadata associated with them in the format
// that ocid understands. The API it provides should be considered to be
// unstable.
package storage

195
pkg/storage/image.go Normal file
View file

@ -0,0 +1,195 @@
package storage
import (
"github.com/containers/image/copy"
"github.com/containers/image/signature"
istorage "github.com/containers/image/storage"
"github.com/containers/image/transports"
"github.com/containers/image/types"
"github.com/containers/storage/storage"
)
// ImageResult wraps a subset of information about an image: its ID, its names,
// and the size, if known, or nil if it isn't.
type ImageResult struct {
ID string
Names []string
Size *uint64
}
type imageService struct {
store storage.Store
defaultTransport string
}
// ImageServer wraps up various CRI-related activities into a reusable
// implementation.
type ImageServer interface {
// ListImages returns list of all images which match the filter.
ListImages(filter string) ([]ImageResult, error)
// ImageStatus returns status of an image which matches the filter.
ImageStatus(systemContext *types.SystemContext, filter string) (*ImageResult, error)
// PullImage imports an image from the specified location.
PullImage(systemContext *types.SystemContext, imageName string, options *copy.Options) (types.ImageReference, error)
// RemoveImage deletes the specified image.
RemoveImage(systemContext *types.SystemContext, imageName string) error
// GetStore returns the reference to the storage library Store which
// the image server uses to hold images, and is the destination used
// when it's asked to pull an image.
GetStore() storage.Store
}
func (svc *imageService) ListImages(filter string) ([]ImageResult, error) {
results := []ImageResult{}
if filter != "" {
if image, err := svc.store.GetImage(filter); err == nil {
results = append(results, ImageResult{
ID: image.ID,
Names: image.Names,
})
}
} else {
images, err := svc.store.Images()
if err != nil {
return nil, err
}
for _, image := range images {
results = append(results, ImageResult{
ID: image.ID,
Names: image.Names,
})
}
}
return results, nil
}
func (svc *imageService) ImageStatus(systemContext *types.SystemContext, nameOrID string) (*ImageResult, error) {
ref, err := transports.ParseImageName(nameOrID)
if err != nil {
ref2, err2 := istorage.Transport.ParseStoreReference(svc.store, "@"+nameOrID)
if err2 != nil {
ref3, err3 := istorage.Transport.ParseStoreReference(svc.store, nameOrID)
if err3 != nil {
return nil, err
}
ref2 = ref3
}
ref = ref2
}
image, err := istorage.Transport.GetStoreImage(svc.store, ref)
if err != nil {
return nil, err
}
img, err := ref.NewImage(systemContext)
if err != nil {
return nil, err
}
size := imageSize(img)
img.Close()
return &ImageResult{
ID: image.ID,
Names: image.Names,
Size: size,
}, nil
}
func imageSize(img types.Image) *uint64 {
if sum, err := img.Size(); err == nil {
usum := uint64(sum)
return &usum
}
return nil
}
func (svc *imageService) PullImage(systemContext *types.SystemContext, imageName string, options *copy.Options) (types.ImageReference, error) {
policy, err := signature.DefaultPolicy(systemContext)
if err != nil {
return nil, err
}
policyContext, err := signature.NewPolicyContext(policy)
if err != nil {
return nil, err
}
if imageName == "" {
return nil, storage.ErrNotAnImage
}
if options == nil {
options = &copy.Options{}
}
srcRef, err := transports.ParseImageName(imageName)
if err != nil {
if svc.defaultTransport == "" {
return nil, err
}
srcRef2, err2 := transports.ParseImageName(svc.defaultTransport + imageName)
if err2 != nil {
return nil, err
}
srcRef = srcRef2
}
dest := imageName
if srcRef.DockerReference() != nil {
dest = srcRef.DockerReference().FullName()
}
destRef, err := istorage.Transport.ParseStoreReference(svc.store, dest)
if err != nil {
return nil, err
}
err = copy.Image(policyContext, destRef, srcRef, options)
if err != nil {
return nil, err
}
// Go find the image, and attach the requested name to it, so that we
// can more easily find it later, even if the destination reference
// looks different.
destImage, err := istorage.Transport.GetStoreImage(svc.store, destRef)
if err != nil {
return nil, err
}
names := append(destImage.Names, imageName, dest)
err = svc.store.SetNames(destImage.ID, names)
if err != nil {
return nil, err
}
return destRef, nil
}
func (svc *imageService) RemoveImage(systemContext *types.SystemContext, nameOrID string) error {
ref, err := transports.ParseImageName(nameOrID)
if err != nil {
ref2, err2 := istorage.Transport.ParseStoreReference(svc.store, "@"+nameOrID)
if err2 != nil {
ref3, err3 := istorage.Transport.ParseStoreReference(svc.store, nameOrID)
if err3 != nil {
return err
}
ref2 = ref3
}
ref = ref2
}
return ref.DeleteImage(systemContext)
}
func (svc *imageService) GetStore() storage.Store {
return svc.store
}
// GetImageService returns an ImageServer that uses the passed-in store, and
// which will prepend the passed-in defaultTransport value to an image name if
// a name that's passed to its PullImage() method can't be resolved to an image
// in the store and can't be resolved to a source on its own.
func GetImageService(store storage.Store, defaultTransport string) (ImageServer, error) {
if store == nil {
var err error
store, err = storage.GetStore(storage.DefaultStoreOptions)
if err != nil {
return nil, err
}
}
return &imageService{
store: store,
defaultTransport: defaultTransport,
}, nil
}

456
pkg/storage/runtime.go Normal file
View file

@ -0,0 +1,456 @@
package storage
import (
"encoding/json"
"errors"
"time"
"github.com/Sirupsen/logrus"
"github.com/containers/image/copy"
istorage "github.com/containers/image/storage"
"github.com/containers/image/transports"
"github.com/containers/image/types"
"github.com/containers/storage/storage"
"github.com/opencontainers/image-spec/specs-go/v1"
)
var (
// ErrInvalidPodName is returned when a pod name specified to a
// function call is found to be invalid (most often, because it's
// empty).
ErrInvalidPodName = errors.New("invalid pod name")
// ErrInvalidImageName is returned when an image name specified to a
// function call is found to be invalid (most often, because it's
// empty).
ErrInvalidImageName = errors.New("invalid image name")
// ErrInvalidContainerName is returned when a container name specified
// to a function call is found to be invalid (most often, because it's
// empty).
ErrInvalidContainerName = errors.New("invalid container name")
// ErrInvalidSandboxID is returned when a sandbox ID specified to a
// function call is found to be invalid (because it's either
// empty or doesn't match a valid sandbox).
ErrInvalidSandboxID = errors.New("invalid sandbox ID")
// ErrInvalidContainerID is returned when a container ID specified to a
// function call is found to be invalid (because it's either
// empty or doesn't match a valid container).
ErrInvalidContainerID = errors.New("invalid container ID")
)
type runtimeService struct {
image ImageServer
}
// ContainerInfo wraps a subset of information about a container: its ID and
// 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 {
ID string
Dir string
RunDir string
Config *v1.Image
}
// RuntimeServer wraps up various CRI-related activities into a reusable
// implementation.
type RuntimeServer interface {
// CreatePodSandbox creates a pod infrastructure container, using the
// specified PodID for the infrastructure container's ID. In the CRI
// view of things, a sandbox is distinct from its containers, including
// its infrastructure container, but at this level the sandbox is
// essentially the same as its infrastructure container, with a
// container's membership in a pod being signified by it listing the
// same pod ID in its metadata that the pod's other members do, and
// with the pod's infrastructure container having the same value for
// both its pod's ID and its container ID.
// Pointer arguments can be nil. Either the image name or ID can be
// omitted, but not both. All other arguments are required.
CreatePodSandbox(systemContext *types.SystemContext, podName, podID, imageName, imageID, containerName, metadataName, uid, namespace string, attempt uint32, copyOptions *copy.Options) (ContainerInfo, error)
// RemovePodSandbox deletes a pod sandbox's infrastructure container.
// The CRI expects that a sandbox can't be removed unless its only
// container is its infrastructure container, but we don't enforce that
// here, since we're just keeping track of it for higher level APIs.
RemovePodSandbox(idOrName string) error
// GetContainerMetadata returns the metadata we've stored for a container.
GetContainerMetadata(idOrName string) (RuntimeContainerMetadata, error)
// SetContainerMetadata updates the metadata we've stored for a container.
SetContainerMetadata(idOrName string, metadata RuntimeContainerMetadata) error
// CreateContainer creates a container with the specified ID.
// Pointer arguments can be nil. Either the image name or ID can be
// omitted, but not both. All other arguments are required.
CreateContainer(systemContext *types.SystemContext, podName, podID, imageName, imageID, containerName, containerID, metadataName string, attempt uint32, mountLabel string, copyOptions *copy.Options) (ContainerInfo, error)
// DeleteContainer deletes a container, unmounting it first if need be.
DeleteContainer(idOrName string) error
// StartContainer makes sure a container's filesystem is mounted, and
// returns the location of its root filesystem, which is not guaranteed
// by lower-level drivers to never change.
StartContainer(idOrName string) (string, error)
// StopContainer attempts to unmount a container's root filesystem,
// freeing up any kernel resources which may be limited.
StopContainer(idOrName string) error
// GetWorkDir returns the path of a nonvolatile directory on the
// filesystem (somewhere under the Store's Root directory) which can be
// used to store arbitrary data that is specific to the container. It
// will be removed automatically when the container is deleted.
GetWorkDir(id string) (string, error)
// GetRunDir returns the path of a volatile directory (does not survive
// the host rebooting, somewhere under the Store's RunRoot directory)
// on the filesystem which can be used to store arbitrary data that is
// specific to the container. It will be removed automatically when
// the container is deleted.
GetRunDir(id string) (string, error)
}
// 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 of pod sandboxes and 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
// RuntimeServer.GetContainerMetadata, RuntimeContainerMetadata.SetMountLabel,
// and RuntimeServer.SetContainerMetadata.
type RuntimeContainerMetadata struct {
// Pod is true if this is the pod's infrastructure container.
Pod bool `json:"pod,omitempty"` // Applicable to both PodSandboxes and Containers
// The pod's name and ID, kept for use by upper layers in determining
// which containers belong to which pods.
PodName string `json:"pod-name"` // Applicable to both PodSandboxes and Containers, mandatory
PodID string `json:"pod-id"` // Applicable to both PodSandboxes and Containers, mandatory
// 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
// The name as originally specified in PodSandbox or Container CRI metadata.
MetadataName string `json:"metadata-name"` // Applicable to both PodSandboxes and Containers, mandatory
UID string `json:"uid,omitempty"` // Only applicable to pods
Namespace string `json:"namespace,omitempty"` // Only applicable to pods
Attempt uint32 `json:"attempt,omitempty"` // Applicable to both PodSandboxes and Containers
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
}
func (r *runtimeService) createContainerOrPodSandbox(systemContext *types.SystemContext, podName, podID, imageName, imageID, containerName, containerID, metadataName, uid, namespace string, attempt uint32, mountLabel string, options *copy.Options) (ContainerInfo, error) {
var ref types.ImageReference
if podName == "" || podID == "" {
return ContainerInfo{}, ErrInvalidPodName
}
if imageName == "" && imageID == "" {
return ContainerInfo{}, ErrInvalidImageName
}
if containerName == "" {
return ContainerInfo{}, ErrInvalidContainerName
}
if metadataName == "" {
metadataName = containerName
}
// Check if we have the specified image.
ref, err := istorage.Transport.ParseStoreReference(r.image.GetStore(), imageName)
if err != nil {
// Maybe it's some other transport's copy of the image?
otherRef, err2 := transports.ParseImageName(imageName)
if err2 == nil && otherRef.DockerReference() != nil {
ref, err = istorage.Transport.ParseStoreReference(r.image.GetStore(), otherRef.DockerReference().FullName())
}
if err != nil {
return ContainerInfo{}, err
}
}
img, err := istorage.Transport.GetStoreImage(r.image.GetStore(), ref)
if img == nil && err == storage.ErrImageUnknown && imageID != "" {
ref, err = istorage.Transport.ParseStoreReference(r.image.GetStore(), "@"+imageID)
if err != nil {
return ContainerInfo{}, err
}
img, err = istorage.Transport.GetStoreImage(r.image.GetStore(), ref)
}
if err != nil && err != storage.ErrImageUnknown {
return ContainerInfo{}, err
}
// Pull the image down if we don't already have it.
if err == storage.ErrImageUnknown {
image := imageID
if imageName != "" {
image = imageName
}
if image == "" {
return ContainerInfo{}, ErrInvalidImageName
}
logrus.Debugf("couldn't find image %q, retrieving it", image)
ref, err = r.image.PullImage(systemContext, image, options)
if err != nil {
return ContainerInfo{}, err
}
img, err = istorage.Transport.GetStoreImage(r.image.GetStore(), ref)
if err != nil {
return ContainerInfo{}, err
}
logrus.Debugf("successfully pulled image %q", image)
}
// Pull out a copy of the image's configuration.
image, err := ref.NewImage(systemContext)
if err != nil {
return ContainerInfo{}, err
}
defer image.Close()
var imageConfig *v1.Image
configBlob, err := image.ConfigBlob()
if err != nil {
return ContainerInfo{}, err
}
if len(configBlob) > 0 {
config := v1.Image{}
err = json.Unmarshal(configBlob, &config)
if err != nil {
return ContainerInfo{}, err
}
imageConfig = &config
}
// 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{
Pod: containerID == podID,
PodName: podName,
PodID: podID,
ImageName: imageName,
ImageID: imageID,
ContainerName: containerName,
MetadataName: metadataName,
UID: uid,
Namespace: namespace,
Attempt: attempt,
CreatedAt: time.Now().Unix(),
MountLabel: mountLabel,
}
mdata, err := json.Marshal(&metadata)
if err != nil {
return ContainerInfo{}, err
}
// Build the container.
names := []string{metadata.ContainerName}
if metadata.Pod {
names = append(names, metadata.PodName)
}
container, err := r.image.GetStore().CreateContainer(containerID, names, img.ID, "", string(mdata), nil)
if err != nil {
if metadata.Pod {
logrus.Debugf("failed to create pod sandbox %s(%s): %v", metadata.PodName, metadata.PodID, err)
} else {
logrus.Debugf("failed to create container %s(%s): %v", metadata.ContainerName, containerID, err)
}
return ContainerInfo{}, err
}
if metadata.Pod {
logrus.Debugf("created pod sandbox %q", container.ID)
} else {
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.image.GetStore().DeleteContainer(container.ID); err2 != nil {
if metadata.Pod {
logrus.Infof("%v deleting partially-created pod sandbox %q", err2, container.ID)
} else {
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.image.GetStore().GetNames(container.LayerID)
if err != nil {
return ContainerInfo{}, err
}
names = append(names, layerName)
err = r.image.GetStore().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.image.GetStore().GetContainerDirectory(container.ID)
if err != nil {
return ContainerInfo{}, err
}
if metadata.Pod {
logrus.Debugf("pod sandbox %q has work directory %q", container.ID, containerDir)
} else {
logrus.Debugf("container %q has work directory %q", container.ID, containerDir)
}
containerRunDir, err := r.image.GetStore().GetContainerRunDirectory(container.ID)
if err != nil {
return ContainerInfo{}, err
}
if metadata.Pod {
logrus.Debugf("pod sandbox %q has run directory %q", container.ID, containerRunDir)
} else {
logrus.Debugf("container %q has run directory %q", container.ID, containerRunDir)
}
return ContainerInfo{
ID: container.ID,
Dir: containerDir,
RunDir: containerRunDir,
Config: imageConfig,
}, nil
}
func (r *runtimeService) CreatePodSandbox(systemContext *types.SystemContext, podName, podID, imageName, imageID, containerName, metadataName, uid, namespace string, attempt uint32, copyOptions *copy.Options) (ContainerInfo, error) {
return r.createContainerOrPodSandbox(systemContext, podName, podID, imageName, imageID, containerName, podID, metadataName, uid, namespace, attempt, "", copyOptions)
}
func (r *runtimeService) CreateContainer(systemContext *types.SystemContext, podName, podID, imageName, imageID, containerName, containerID, metadataName string, attempt uint32, mountLabel string, copyOptions *copy.Options) (ContainerInfo, error) {
return r.createContainerOrPodSandbox(systemContext, podName, podID, imageName, imageID, containerName, containerID, metadataName, "", "", attempt, mountLabel, copyOptions)
}
func (r *runtimeService) RemovePodSandbox(idOrName string) error {
container, err := r.image.GetStore().GetContainer(idOrName)
if err != nil {
if err == storage.ErrContainerUnknown {
return ErrInvalidSandboxID
}
return err
}
err = r.image.GetStore().DeleteContainer(container.ID)
if err != nil {
logrus.Debugf("failed to delete pod sandbox %q: %v", container.ID, err)
return err
}
return nil
}
func (r *runtimeService) DeleteContainer(idOrName string) error {
container, err := r.image.GetStore().GetContainer(idOrName)
if err != nil {
if err == storage.ErrContainerUnknown {
return ErrInvalidContainerID
}
return err
}
err = r.image.GetStore().DeleteContainer(container.ID)
if err != nil {
logrus.Debugf("failed to delete container %q: %v", container.ID, err)
return err
}
return nil
}
func (r *runtimeService) 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.image.GetStore().SetMetadata(idOrName, string(mdata))
}
func (r *runtimeService) GetContainerMetadata(idOrName string) (RuntimeContainerMetadata, error) {
metadata := RuntimeContainerMetadata{}
mdata, err := r.image.GetStore().GetMetadata(idOrName)
if err != nil {
return metadata, err
}
if err = json.Unmarshal([]byte(mdata), &metadata); err != nil {
return metadata, err
}
return metadata, nil
}
func (r *runtimeService) StartContainer(idOrName string) (string, error) {
container, err := r.image.GetStore().GetContainer(idOrName)
if err != nil {
if err == storage.ErrContainerUnknown {
return "", ErrInvalidContainerID
}
return "", err
}
metadata := RuntimeContainerMetadata{}
if err = json.Unmarshal([]byte(container.Metadata), &metadata); err != nil {
return "", err
}
mountPoint, err := r.image.GetStore().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 *runtimeService) StopContainer(idOrName string) error {
container, err := r.image.GetStore().GetContainer(idOrName)
if err != nil {
if err == storage.ErrContainerUnknown {
return ErrInvalidContainerID
}
return err
}
err = r.image.GetStore().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 *runtimeService) GetWorkDir(id string) (string, error) {
container, err := r.image.GetStore().GetContainer(id)
if err != nil {
if err == storage.ErrContainerUnknown {
return "", ErrInvalidContainerID
}
return "", err
}
return r.image.GetStore().GetContainerDirectory(container.ID)
}
func (r *runtimeService) GetRunDir(id string) (string, error) {
container, err := r.image.GetStore().GetContainer(id)
if err != nil {
if err == storage.ErrContainerUnknown {
return "", ErrInvalidContainerID
}
return "", err
}
return r.image.GetStore().GetContainerRunDirectory(container.ID)
}
// GetRuntimeService returns a RuntimeServer that uses the passed-in image
// service to pull and manage images, and its store to manage containers based
// on those images.
func GetRuntimeService(image ImageServer) RuntimeServer {
return &runtimeService{
image: image,
}
}