diff options
author | Matthew Heon <matthew.heon@gmail.com> | 2017-11-01 11:24:59 -0400 |
---|---|---|
committer | Matthew Heon <matthew.heon@gmail.com> | 2017-11-01 11:24:59 -0400 |
commit | a031b83a09a8628435317a03f199cdc18b78262f (patch) | |
tree | bc017a96769ce6de33745b8b0b1304ccf38e9df0 /pkg | |
parent | 2b74391cd5281f6fdf391ff8ad50fd1490f6bf89 (diff) | |
download | podman-a031b83a09a8628435317a03f199cdc18b78262f.tar.gz podman-a031b83a09a8628435317a03f199cdc18b78262f.tar.bz2 podman-a031b83a09a8628435317a03f199cdc18b78262f.zip |
Initial checkin from CRI-O repo
Signed-off-by: Matthew Heon <matthew.heon@gmail.com>
Diffstat (limited to 'pkg')
-rw-r--r-- | pkg/annotations/annotations.go | 93 | ||||
-rw-r--r-- | pkg/registrar/registrar.go | 127 | ||||
-rw-r--r-- | pkg/registrar/registrar_test.go | 119 | ||||
-rw-r--r-- | pkg/storage/doc.go | 5 | ||||
-rw-r--r-- | pkg/storage/image.go | 551 | ||||
-rw-r--r-- | pkg/storage/image_regexp.go | 125 | ||||
-rw-r--r-- | pkg/storage/runtime.go | 452 |
7 files changed, 1472 insertions, 0 deletions
diff --git a/pkg/annotations/annotations.go b/pkg/annotations/annotations.go new file mode 100644 index 000000000..151d93904 --- /dev/null +++ b/pkg/annotations/annotations.go @@ -0,0 +1,93 @@ +package annotations + +const ( + // Annotations carries the received Kubelet annotations + Annotations = "io.kubernetes.cri-o.Annotations" + + // ContainerID is the container ID annotation + ContainerID = "io.kubernetes.cri-o.ContainerID" + + // ContainerName is the container name annotation + ContainerName = "io.kubernetes.cri-o.ContainerName" + + // ContainerType is the container type (sandbox or container) annotation + ContainerType = "io.kubernetes.cri-o.ContainerType" + + // Created is the container creation time annotation + Created = "io.kubernetes.cri-o.Created" + + // HostName is the container host name annotation + HostName = "io.kubernetes.cri-o.HostName" + + // IP is the container ipv4 or ipv6 address + IP = "io.kubernetes.cri-o.IP" + + // Image is the container image ID annotation + Image = "io.kubernetes.cri-o.Image" + + // ImageName is the container image name annotation + ImageName = "io.kubernetes.cri-o.ImageName" + + // ImageRef is the container image ref annotation + ImageRef = "io.kubernetes.cri-o.ImageRef" + + // KubeName is the kubernetes name annotation + KubeName = "io.kubernetes.cri-o.KubeName" + + // Labels are the kubernetes labels annotation + Labels = "io.kubernetes.cri-o.Labels" + + // LogPath is the container logging path annotation + LogPath = "io.kubernetes.cri-o.LogPath" + + // Metadata is the container metadata annotation + Metadata = "io.kubernetes.cri-o.Metadata" + + // Name is the pod name annotation + Name = "io.kubernetes.cri-o.Name" + + // PrivilegedRuntime is the annotation for the privileged runtime path + PrivilegedRuntime = "io.kubernetes.cri-o.PrivilegedRuntime" + + // ResolvPath is the resolver configuration path annotation + ResolvPath = "io.kubernetes.cri-o.ResolvPath" + + // HostnamePath is the path to /etc/hostname to bind mount annotation + HostnamePath = "io.kubernetes.cri-o.HostnamePath" + + // SandboxID is the sandbox ID annotation + SandboxID = "io.kubernetes.cri-o.SandboxID" + + // SandboxName is the sandbox name annotation + SandboxName = "io.kubernetes.cri-o.SandboxName" + + // ShmPath is the shared memory path annotation + ShmPath = "io.kubernetes.cri-o.ShmPath" + + // MountPoint is the mount point of the container rootfs + MountPoint = "io.kubernetes.cri-o.MountPoint" + + // TrustedSandbox is the annotation for trusted sandboxes + TrustedSandbox = "io.kubernetes.cri-o.TrustedSandbox" + + // TTY is the terminal path annotation + TTY = "io.kubernetes.cri-o.TTY" + + // Stdin is the stdin annotation + Stdin = "io.kubernetes.cri-o.Stdin" + + // StdinOnce is the stdin_once annotation + StdinOnce = "io.kubernetes.cri-o.StdinOnce" + + // Volumes is the volumes annotatoin + Volumes = "io.kubernetes.cri-o.Volumes" +) + +// ContainerType values +const ( + // ContainerTypeSandbox represents a pod sandbox container + ContainerTypeSandbox = "sandbox" + + // ContainerTypeContainer represents a container running within a pod + ContainerTypeContainer = "container" +) diff --git a/pkg/registrar/registrar.go b/pkg/registrar/registrar.go new file mode 100644 index 000000000..1e75ee995 --- /dev/null +++ b/pkg/registrar/registrar.go @@ -0,0 +1,127 @@ +// Package registrar provides name registration. It reserves a name to a given key. +package registrar + +import ( + "errors" + "sync" +) + +var ( + // ErrNameReserved is an error which is returned when a name is requested to be reserved that already is reserved + ErrNameReserved = errors.New("name is reserved") + // ErrNameNotReserved is an error which is returned when trying to find a name that is not reserved + ErrNameNotReserved = errors.New("name is not reserved") + // ErrNoSuchKey is returned when trying to find the names for a key which is not known + ErrNoSuchKey = errors.New("provided key does not exist") +) + +// Registrar stores indexes a list of keys and their registered names as well as indexes names and the key that they are registered to +// Names must be unique. +// Registrar is safe for concurrent access. +type Registrar struct { + idx map[string][]string + names map[string]string + mu sync.Mutex +} + +// NewRegistrar creates a new Registrar with the an empty index +func NewRegistrar() *Registrar { + return &Registrar{ + idx: make(map[string][]string), + names: make(map[string]string), + } +} + +// Reserve registers a key to a name +// Reserve is idempotent +// Attempting to reserve a key to a name that already exists results in an `ErrNameReserved` +// A name reservation is globally unique +func (r *Registrar) Reserve(name, key string) error { + r.mu.Lock() + defer r.mu.Unlock() + + if k, exists := r.names[name]; exists { + if k != key { + return ErrNameReserved + } + return nil + } + + r.idx[key] = append(r.idx[key], name) + r.names[name] = key + return nil +} + +// Release releases the reserved name +// Once released, a name can be reserved again +func (r *Registrar) Release(name string) { + r.mu.Lock() + defer r.mu.Unlock() + + key, exists := r.names[name] + if !exists { + return + } + + for i, n := range r.idx[key] { + if n != name { + continue + } + r.idx[key] = append(r.idx[key][:i], r.idx[key][i+1:]...) + break + } + + delete(r.names, name) + + if len(r.idx[key]) == 0 { + delete(r.idx, key) + } +} + +// Delete removes all reservations for the passed in key. +// All names reserved to this key are released. +func (r *Registrar) Delete(key string) { + r.mu.Lock() + for _, name := range r.idx[key] { + delete(r.names, name) + } + delete(r.idx, key) + r.mu.Unlock() +} + +// GetNames lists all the reserved names for the given key +func (r *Registrar) GetNames(key string) ([]string, error) { + r.mu.Lock() + defer r.mu.Unlock() + + names, exists := r.idx[key] + if !exists { + return nil, ErrNoSuchKey + } + return names, nil +} + +// Get returns the key that the passed in name is reserved to +func (r *Registrar) Get(name string) (string, error) { + r.mu.Lock() + key, exists := r.names[name] + r.mu.Unlock() + + if !exists { + return "", ErrNameNotReserved + } + return key, nil +} + +// GetAll returns all registered names +func (r *Registrar) GetAll() map[string][]string { + out := make(map[string][]string) + + r.mu.Lock() + // copy index into out + for id, names := range r.idx { + out[id] = names + } + r.mu.Unlock() + return out +} diff --git a/pkg/registrar/registrar_test.go b/pkg/registrar/registrar_test.go new file mode 100644 index 000000000..0c1ef312a --- /dev/null +++ b/pkg/registrar/registrar_test.go @@ -0,0 +1,119 @@ +package registrar + +import ( + "reflect" + "testing" +) + +func TestReserve(t *testing.T) { + r := NewRegistrar() + + obj := "test1" + if err := r.Reserve("test", obj); err != nil { + t.Fatal(err) + } + + if err := r.Reserve("test", obj); err != nil { + t.Fatal(err) + } + + obj2 := "test2" + err := r.Reserve("test", obj2) + if err == nil { + t.Fatalf("expected error when reserving an already reserved name to another object") + } + if err != ErrNameReserved { + t.Fatal("expected `ErrNameReserved` error when attempting to reserve an already reserved name") + } +} + +func TestRelease(t *testing.T) { + r := NewRegistrar() + obj := "testing" + + if err := r.Reserve("test", obj); err != nil { + t.Fatal(err) + } + r.Release("test") + r.Release("test") // Ensure there is no panic here + + if err := r.Reserve("test", obj); err != nil { + t.Fatal(err) + } +} + +func TestGetNames(t *testing.T) { + r := NewRegistrar() + obj := "testing" + names := []string{"test1", "test2"} + + for _, name := range names { + if err := r.Reserve(name, obj); err != nil { + t.Fatal(err) + } + } + r.Reserve("test3", "other") + + names2, err := r.GetNames(obj) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(names, names2) { + t.Fatalf("Exepected: %v, Got: %v", names, names2) + } +} + +func TestDelete(t *testing.T) { + r := NewRegistrar() + obj := "testing" + names := []string{"test1", "test2"} + for _, name := range names { + if err := r.Reserve(name, obj); err != nil { + t.Fatal(err) + } + } + + r.Reserve("test3", "other") + r.Delete(obj) + + _, err := r.GetNames(obj) + if err == nil { + t.Fatal("expected error getting names for deleted key") + } + + if err != ErrNoSuchKey { + t.Fatal("expected `ErrNoSuchKey`") + } +} + +func TestGet(t *testing.T) { + r := NewRegistrar() + obj := "testing" + name := "test" + + _, err := r.Get(name) + if err == nil { + t.Fatal("expected error when key does not exist") + } + if err != ErrNameNotReserved { + t.Fatal(err) + } + + if err := r.Reserve(name, obj); err != nil { + t.Fatal(err) + } + + if _, err = r.Get(name); err != nil { + t.Fatal(err) + } + + r.Delete(obj) + _, err = r.Get(name) + if err == nil { + t.Fatal("expected error when key does not exist") + } + if err != ErrNameNotReserved { + t.Fatal(err) + } +} diff --git a/pkg/storage/doc.go b/pkg/storage/doc.go new file mode 100644 index 000000000..6366b22a4 --- /dev/null +++ b/pkg/storage/doc.go @@ -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 crio understands. The API it provides should be considered to be +// unstable. +package storage diff --git a/pkg/storage/image.go b/pkg/storage/image.go new file mode 100644 index 000000000..ddff82c1c --- /dev/null +++ b/pkg/storage/image.go @@ -0,0 +1,551 @@ +package storage + +import ( + "errors" + "fmt" + "net" + "path" + "regexp" + "strings" + + "github.com/containers/image/copy" + "github.com/containers/image/docker/reference" + "github.com/containers/image/image" + "github.com/containers/image/signature" + istorage "github.com/containers/image/storage" + "github.com/containers/image/transports/alltransports" + "github.com/containers/image/types" + "github.com/containers/storage" + distreference "github.com/docker/distribution/reference" +) + +// 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 + Digests []string + Size *uint64 + ImageRef string +} + +type indexInfo struct { + name string + secure bool +} + +type imageService struct { + store storage.Store + defaultTransport string + insecureRegistryCIDRs []*net.IPNet + indexConfigs map[string]*indexInfo + registries []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(systemContext *types.SystemContext, 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) + // UntagImage removes a name from the specified image, and if it was + // the only name the image had, removes the image. + UntagImage(systemContext *types.SystemContext, imageName string) 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 + // CanPull preliminary checks whether we're allowed to pull an image + CanPull(imageName string, options *copy.Options) (bool, error) + // ResolveNames takes an image reference and if it's unqualified (w/o hostname), + // it uses crio's default registries to qualify it. + ResolveNames(imageName string) ([]string, error) +} + +func (svc *imageService) getRef(name string) (types.ImageReference, error) { + ref, err := alltransports.ParseImageName(name) + if err != nil { + ref2, err2 := istorage.Transport.ParseStoreReference(svc.store, "@"+name) + if err2 != nil { + ref3, err3 := istorage.Transport.ParseStoreReference(svc.store, name) + if err3 != nil { + return nil, err + } + ref2 = ref3 + } + ref = ref2 + } + return ref, nil +} + +func (svc *imageService) ListImages(systemContext *types.SystemContext, filter string) ([]ImageResult, error) { + results := []ImageResult{} + if filter != "" { + ref, err := svc.getRef(filter) + if err != nil { + return nil, err + } + if image, err := istorage.Transport.GetStoreImage(svc.store, ref); err == nil { + img, err := ref.NewImage(systemContext) + if err != nil { + return nil, err + } + size := imageSize(img) + img.Close() + results = append(results, ImageResult{ + ID: image.ID, + Names: image.Names, + Size: size, + }) + } + } else { + images, err := svc.store.Images() + if err != nil { + return nil, err + } + for _, image := range images { + ref, err := istorage.Transport.ParseStoreReference(svc.store, "@"+image.ID) + if err != nil { + return nil, err + } + img, err := ref.NewImage(systemContext) + if err != nil { + return nil, err + } + size := imageSize(img) + img.Close() + results = append(results, ImageResult{ + ID: image.ID, + Names: image.Names, + Size: size, + }) + } + } + return results, nil +} + +func (svc *imageService) ImageStatus(systemContext *types.SystemContext, nameOrID string) (*ImageResult, error) { + ref, err := alltransports.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() + + result := ImageResult{ + ID: image.ID, + Names: image.Names, + Size: size, + } + if len(image.Names) > 0 { + result.ImageRef = image.Names[0] + if ref2, err2 := istorage.Transport.ParseStoreReference(svc.store, image.Names[0]); err2 == nil { + if dref := ref2.DockerReference(); dref != nil { + result.ImageRef = reference.FamiliarString(dref) + } + } + } + + return &result, nil +} + +func imageSize(img types.Image) *uint64 { + if sum, err := img.Size(); err == nil { + usum := uint64(sum) + return &usum + } + return nil +} + +func (svc *imageService) CanPull(imageName string, options *copy.Options) (bool, error) { + srcRef, err := svc.prepareImage(imageName, options) + if err != nil { + return false, err + } + rawSource, err := srcRef.NewImageSource(options.SourceCtx) + if err != nil { + return false, err + } + src, err := image.FromSource(rawSource) + if err != nil { + rawSource.Close() + return false, err + } + src.Close() + return true, nil +} + +// prepareImage creates an image reference from an image string and set options +// for the source context +func (svc *imageService) prepareImage(imageName string, options *copy.Options) (types.ImageReference, error) { + if imageName == "" { + return nil, storage.ErrNotAnImage + } + + srcRef, err := alltransports.ParseImageName(imageName) + if err != nil { + if svc.defaultTransport == "" { + return nil, err + } + srcRef2, err2 := alltransports.ParseImageName(svc.defaultTransport + imageName) + if err2 != nil { + return nil, err + } + srcRef = srcRef2 + } + + if options.SourceCtx == nil { + options.SourceCtx = &types.SystemContext{} + } + + hostname := reference.Domain(srcRef.DockerReference()) + if secure := svc.isSecureIndex(hostname); !secure { + options.SourceCtx.DockerInsecureSkipTLSVerify = !secure + } + return srcRef, 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 options == nil { + options = ©.Options{} + } + + srcRef, err := svc.prepareImage(imageName, options) + if err != nil { + return nil, err + } + + dest := imageName + if srcRef.DockerReference() != nil { + dest = srcRef.DockerReference().Name() + if tagged, ok := srcRef.DockerReference().(reference.NamedTagged); ok { + dest = dest + ":" + tagged.Tag() + } + if canonical, ok := srcRef.DockerReference().(reference.Canonical); ok { + dest = dest + "@" + canonical.Digest().String() + } + } + 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 + } + return destRef, nil +} + +func (svc *imageService) UntagImage(systemContext *types.SystemContext, nameOrID string) error { + ref, err := alltransports.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 + } + + img, err := istorage.Transport.GetStoreImage(svc.store, ref) + if err != nil { + return err + } + + if nameOrID != img.ID { + namedRef, err := svc.prepareImage(nameOrID, ©.Options{}) + if err != nil { + return err + } + + name := nameOrID + if namedRef.DockerReference() != nil { + name = namedRef.DockerReference().Name() + if tagged, ok := namedRef.DockerReference().(reference.NamedTagged); ok { + name = name + ":" + tagged.Tag() + } + if canonical, ok := namedRef.DockerReference().(reference.Canonical); ok { + name = name + "@" + canonical.Digest().String() + } + } + + prunedNames := make([]string, 0, len(img.Names)) + for _, imgName := range img.Names { + if imgName != name && imgName != nameOrID { + prunedNames = append(prunedNames, imgName) + } + } + + if len(prunedNames) > 0 { + return svc.store.SetNames(img.ID, prunedNames) + } + } + + return ref.DeleteImage(systemContext) +} + +func (svc *imageService) RemoveImage(systemContext *types.SystemContext, nameOrID string) error { + ref, err := alltransports.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 +} + +func (svc *imageService) isSecureIndex(indexName string) bool { + if index, ok := svc.indexConfigs[indexName]; ok { + return index.secure + } + + host, _, err := net.SplitHostPort(indexName) + if err != nil { + // assume indexName is of the form `host` without the port and go on. + host = indexName + } + + addrs, err := net.LookupIP(host) + if err != nil { + ip := net.ParseIP(host) + if ip != nil { + addrs = []net.IP{ip} + } + + // if ip == nil, then `host` is neither an IP nor it could be looked up, + // either because the index is unreachable, or because the index is behind an HTTP proxy. + // So, len(addrs) == 0 and we're not aborting. + } + + // Try CIDR notation only if addrs has any elements, i.e. if `host`'s IP could be determined. + for _, addr := range addrs { + for _, ipnet := range svc.insecureRegistryCIDRs { + // check if the addr falls in the subnet + if ipnet.Contains(addr) { + return false + } + } + } + + return true +} + +func isValidHostname(hostname string) bool { + return hostname != "" && !strings.Contains(hostname, "/") && + (strings.Contains(hostname, ".") || + strings.Contains(hostname, ":") || hostname == "localhost") +} + +func isReferenceFullyQualified(reposName reference.Named) bool { + indexName, _, _ := splitReposName(reposName) + return indexName != "" +} + +const ( + // defaultHostname is the default built-in hostname + defaultHostname = "docker.io" + // legacyDefaultHostname is automatically converted to DefaultHostname + legacyDefaultHostname = "index.docker.io" + // defaultRepoPrefix is the prefix used for default repositories in default host + defaultRepoPrefix = "library/" +) + +// splitReposName breaks a reposName into an index name and remote name +func splitReposName(reposName reference.Named) (indexName string, remoteName reference.Named, err error) { + var remoteNameStr string + indexName, remoteNameStr = distreference.SplitHostname(reposName) + if !isValidHostname(indexName) { + // This is a Docker Index repos (ex: samalba/hipache or ubuntu) + // 'docker.io' + indexName = "" + remoteName = reposName + } else { + remoteName, err = withName(remoteNameStr) + } + return +} + +func validateName(name string) error { + if err := validateID(strings.TrimPrefix(name, defaultHostname+"/")); err == nil { + return fmt.Errorf("Invalid repository name (%s), cannot specify 64-byte hexadecimal strings", name) + } + return nil +} + +var validHex = regexp.MustCompile(`^([a-f0-9]{64})$`) + +// validateID checks whether an ID string is a valid image ID. +func validateID(id string) error { + if ok := validHex.MatchString(id); !ok { + return fmt.Errorf("image ID %q is invalid", id) + } + return nil +} + +// withName returns a named object representing the given string. If the input +// is invalid ErrReferenceInvalidFormat will be returned. +func withName(name string) (reference.Named, error) { + name, err := normalize(name) + if err != nil { + return nil, err + } + if err := validateName(name); err != nil { + return nil, err + } + r, err := distreference.WithName(name) + return r, err +} + +// splitHostname splits a repository name to hostname and remotename string. +// If no valid hostname is found, empty string will be returned as a resulting +// hostname. Repository name needs to be already validated before. +func splitHostname(name string) (hostname, remoteName string) { + i := strings.IndexRune(name, '/') + if i == -1 || (!strings.ContainsAny(name[:i], ".:") && name[:i] != "localhost") { + hostname, remoteName = "", name + } else { + hostname, remoteName = name[:i], name[i+1:] + } + if hostname == legacyDefaultHostname { + hostname = defaultHostname + } + if hostname == defaultHostname && !strings.ContainsRune(remoteName, '/') { + remoteName = defaultRepoPrefix + remoteName + } + return +} + +// normalize returns a repository name in its normalized form, meaning it +// will contain library/ prefix for official images. +func normalize(name string) (string, error) { + host, remoteName := splitHostname(name) + if strings.ToLower(remoteName) != remoteName { + return "", errors.New("invalid reference format: repository name must be lowercase") + } + if host == defaultHostname { + if strings.HasPrefix(remoteName, defaultRepoPrefix) { + remoteName = strings.TrimPrefix(remoteName, defaultRepoPrefix) + } + return host + "/" + remoteName, nil + } + return name, nil +} + +func (svc *imageService) ResolveNames(imageName string) ([]string, error) { + r, err := reference.ParseNormalizedNamed(imageName) + if err != nil { + return nil, err + } + if isReferenceFullyQualified(r) { + // this means the image is already fully qualified + return []string{imageName}, nil + } + // we got an unqualified image here, we can't go ahead w/o registries configured + // properly. + if len(svc.registries) == 0 { + return nil, errors.New("no registries configured while trying to pull an unqualified image") + } + // this means we got an image in the form of "busybox" + // we need to use additional registries... + // normalize the unqualified image to be domain/repo/image... + _, rest := splitDomain(r.Name()) + images := []string{} + for _, r := range svc.registries { + images = append(images, path.Join(r, rest)) + } + return images, nil +} + +// 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, insecureRegistries []string, registries []string) (ImageServer, error) { + if store == nil { + var err error + store, err = storage.GetStore(storage.DefaultStoreOptions) + if err != nil { + return nil, err + } + } + + seenRegistries := make(map[string]bool, len(registries)) + cleanRegistries := []string{} + for _, r := range registries { + if seenRegistries[r] { + continue + } + cleanRegistries = append(cleanRegistries, r) + seenRegistries[r] = true + } + + is := &imageService{ + store: store, + defaultTransport: defaultTransport, + indexConfigs: make(map[string]*indexInfo, 0), + insecureRegistryCIDRs: make([]*net.IPNet, 0), + registries: cleanRegistries, + } + + insecureRegistries = append(insecureRegistries, "127.0.0.0/8") + // Split --insecure-registry into CIDR and registry-specific settings. + for _, r := range insecureRegistries { + // Check if CIDR was passed to --insecure-registry + _, ipnet, err := net.ParseCIDR(r) + if err == nil { + // Valid CIDR. + is.insecureRegistryCIDRs = append(is.insecureRegistryCIDRs, ipnet) + } else { + // Assume `host:port` if not CIDR. + is.indexConfigs[r] = &indexInfo{ + name: r, + secure: false, + } + } + } + + return is, nil +} diff --git a/pkg/storage/image_regexp.go b/pkg/storage/image_regexp.go new file mode 100644 index 000000000..96de64884 --- /dev/null +++ b/pkg/storage/image_regexp.go @@ -0,0 +1,125 @@ +package storage + +// This is a fork of docker/distribution code to be used when manipulating image +// references. +// DO NOT EDIT THIS FILE. + +import "regexp" + +var ( + // alphaNumericRegexp defines the alpha numeric atom, typically a + // component of names. This only allows lower case characters and digits. + alphaNumericRegexp = match(`[a-z0-9]+`) + + // separatorRegexp defines the separators allowed to be embedded in name + // components. This allow one period, one or two underscore and multiple + // dashes. + separatorRegexp = match(`(?:[._]|__|[-]*)`) + + // nameComponentRegexp restricts registry path component names to start + // with at least one letter or number, with following parts able to be + // separated by one period, one or two underscore and multiple dashes. + nameComponentRegexp = expression( + alphaNumericRegexp, + optional(repeated(separatorRegexp, alphaNumericRegexp))) + + // domainComponentRegexp restricts the registry domain component of a + // repository name to start with a component as defined by domainRegexp + // and followed by an optional port. + domainComponentRegexp = match(`(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])`) + + // domainRegexp defines the structure of potential domain components + // that may be part of image names. This is purposely a subset of what is + // allowed by DNS to ensure backwards compatibility with Docker image + // names. + domainRegexp = expression( + domainComponentRegexp, + optional(repeated(literal(`.`), domainComponentRegexp)), + optional(literal(`:`), match(`[0-9]+`))) + + // NameRegexp is the format for the name component of references. The + // regexp has capturing groups for the domain and name part omitting + // the separating forward slash from either. + NameRegexp = expression( + optional(domainRegexp, literal(`/`)), + nameComponentRegexp, + optional(repeated(literal(`/`), nameComponentRegexp))) + + // anchoredNameRegexp is used to parse a name value, capturing the + // domain and trailing components. + anchoredNameRegexp = anchored( + optional(capture(domainRegexp), literal(`/`)), + capture(nameComponentRegexp, + optional(repeated(literal(`/`), nameComponentRegexp)))) + + // IdentifierRegexp is the format for string identifier used as a + // content addressable identifier using sha256. These identifiers + // are like digests without the algorithm, since sha256 is used. + IdentifierRegexp = match(`([a-f0-9]{64})`) + + // ShortIdentifierRegexp is the format used to represent a prefix + // of an identifier. A prefix may be used to match a sha256 identifier + // within a list of trusted identifiers. + ShortIdentifierRegexp = match(`([a-f0-9]{6,64})`) +) + +// match compiles the string to a regular expression. +var match = regexp.MustCompile + +// literal compiles s into a literal regular expression, escaping any regexp +// reserved characters. +func literal(s string) *regexp.Regexp { + re := match(regexp.QuoteMeta(s)) + + if _, complete := re.LiteralPrefix(); !complete { + panic("must be a literal") + } + + return re +} + +func splitDomain(name string) (string, string) { + match := anchoredNameRegexp.FindStringSubmatch(name) + if len(match) != 3 { + return "", name + } + return match[1], match[2] +} + +// expression defines a full expression, where each regular expression must +// follow the previous. +func expression(res ...*regexp.Regexp) *regexp.Regexp { + var s string + for _, re := range res { + s += re.String() + } + + return match(s) +} + +// optional wraps the expression in a non-capturing group and makes the +// production optional. +func optional(res ...*regexp.Regexp) *regexp.Regexp { + return match(group(expression(res...)).String() + `?`) +} + +// repeated wraps the regexp in a non-capturing group to get one or more +// matches. +func repeated(res ...*regexp.Regexp) *regexp.Regexp { + return match(group(expression(res...)).String() + `+`) +} + +// group wraps the regexp in a non-capturing group. +func group(res ...*regexp.Regexp) *regexp.Regexp { + return match(`(?:` + expression(res...).String() + `)`) +} + +// capture wraps the expression in a capturing group. +func capture(res ...*regexp.Regexp) *regexp.Regexp { + return match(`(` + expression(res...).String() + `)`) +} + +// anchored anchors the regular expression by adding start and end delimiters. +func anchored(res ...*regexp.Regexp) *regexp.Regexp { + return match(`^` + expression(res...).String() + `$`) +} diff --git a/pkg/storage/runtime.go b/pkg/storage/runtime.go new file mode 100644 index 000000000..bc402f413 --- /dev/null +++ b/pkg/storage/runtime.go @@ -0,0 +1,452 @@ +package storage + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/containers/image/copy" + istorage "github.com/containers/image/storage" + "github.com/containers/image/transports/alltransports" + "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" +) + +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 { + storageImageServer ImageServer + pauseImage string +} + +// 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.ContainerMetadata, 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.storageImageServer.GetStore(), imageName) + if err != nil { + // Maybe it's some other transport's copy of the image? + otherRef, err2 := alltransports.ParseImageName(imageName) + if err2 == nil && otherRef.DockerReference() != nil { + ref, err = istorage.Transport.ParseStoreReference(r.storageImageServer.GetStore(), otherRef.DockerReference().Name()) + } + if err != nil { + // Maybe the image ID is sufficient? + ref, err = istorage.Transport.ParseStoreReference(r.storageImageServer.GetStore(), "@"+imageID) + if err != nil { + return ContainerInfo{}, err + } + } + } + img, err := istorage.Transport.GetStoreImage(r.storageImageServer.GetStore(), ref) + if img == nil && errors.Cause(err) == storage.ErrImageUnknown && imageName == r.pauseImage { + image := imageID + if imageName != "" { + image = imageName + } + if image == "" { + return ContainerInfo{}, ErrInvalidImageName + } + logrus.Debugf("couldn't find image %q, retrieving it", image) + ref, err = r.storageImageServer.PullImage(systemContext, image, options) + if err != nil { + return ContainerInfo{}, err + } + img, err = istorage.Transport.GetStoreImage(r.storageImageServer.GetStore(), ref) + if err != nil { + return ContainerInfo{}, err + } + logrus.Debugf("successfully pulled image %q", image) + } + if img == nil && errors.Cause(err) == storage.ErrImageUnknown { + if imageID == "" { + return ContainerInfo{}, fmt.Errorf("image %q not present in image store", imageName) + } + if imageName == "" { + return ContainerInfo{}, fmt.Errorf("image with ID %q not present in image store", imageID) + } + return ContainerInfo{}, fmt.Errorf("image %q with ID %q not present in image store", imageName, imageID) + } + + // 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{ + 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.storageImageServer.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.storageImageServer.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.storageImageServer.GetStore().Names(container.LayerID) + if err != nil { + return ContainerInfo{}, err + } + names = append(names, layerName) + err = r.storageImageServer.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.storageImageServer.GetStore().ContainerDirectory(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.storageImageServer.GetStore().ContainerRunDirectory(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.storageImageServer.GetStore().Container(idOrName) + if err != nil { + if errors.Cause(err) == storage.ErrContainerUnknown { + return ErrInvalidSandboxID + } + return err + } + err = r.storageImageServer.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 { + if idOrName == "" { + return ErrInvalidContainerID + } + container, err := r.storageImageServer.GetStore().Container(idOrName) + if err != nil { + return err + } + err = r.storageImageServer.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.storageImageServer.GetStore().SetMetadata(idOrName, string(mdata)) +} + +func (r *runtimeService) GetContainerMetadata(idOrName string) (RuntimeContainerMetadata, error) { + metadata := RuntimeContainerMetadata{} + mdata, err := r.storageImageServer.GetStore().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 *runtimeService) StartContainer(idOrName string) (string, error) { + container, err := r.storageImageServer.GetStore().Container(idOrName) + if err != nil { + if errors.Cause(err) == storage.ErrContainerUnknown { + return "", ErrInvalidContainerID + } + return "", err + } + metadata := RuntimeContainerMetadata{} + if err = json.Unmarshal([]byte(container.Metadata), &metadata); err != nil { + return "", err + } + mountPoint, err := r.storageImageServer.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 { + if idOrName == "" { + return ErrInvalidContainerID + } + container, err := r.storageImageServer.GetStore().Container(idOrName) + if err != nil { + return err + } + err = r.storageImageServer.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.storageImageServer.GetStore().Container(id) + if err != nil { + if errors.Cause(err) == storage.ErrContainerUnknown { + return "", ErrInvalidContainerID + } + return "", err + } + return r.storageImageServer.GetStore().ContainerDirectory(container.ID) +} + +func (r *runtimeService) GetRunDir(id string) (string, error) { + container, err := r.storageImageServer.GetStore().Container(id) + if err != nil { + if errors.Cause(err) == storage.ErrContainerUnknown { + return "", ErrInvalidContainerID + } + return "", err + } + return r.storageImageServer.GetStore().ContainerRunDirectory(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(storageImageServer ImageServer, pauseImage string) RuntimeServer { + return &runtimeService{ + storageImageServer: storageImageServer, + pauseImage: pauseImage, + } +} |