Merge pull request #675 from stevvooe/images-service
api/services/images: define images metadata service
This commit is contained in:
commit
e2b042e7c1
20 changed files with 1814 additions and 143 deletions
1
api/services/images/docs.go
Normal file
1
api/services/images/docs.go
Normal file
|
@ -0,0 +1 @@
|
|||
package images
|
1355
api/services/images/images.pb.go
Normal file
1355
api/services/images/images.pb.go
Normal file
File diff suppressed because it is too large
Load diff
77
api/services/images/images.proto
Normal file
77
api/services/images/images.proto
Normal file
|
@ -0,0 +1,77 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package containerd.v1;
|
||||
|
||||
import "gogoproto/gogo.proto";
|
||||
import "google/protobuf/empty.proto";
|
||||
import "github.com/containerd/containerd/api/types/mount/mount.proto";
|
||||
import "github.com/containerd/containerd/api/types/descriptor/descriptor.proto";
|
||||
|
||||
// Images is a service that allows one to register images with containerd.
|
||||
//
|
||||
// In containerd, an image is merely the mapping of a name to a content root,
|
||||
// described by a descriptor. The behavior and state of image is purely
|
||||
// dictated by the type of the descriptor.
|
||||
//
|
||||
// From the perspective of this service, these references are mostly shallow,
|
||||
// in that the existence of the required content won't be validated until
|
||||
// required by consuming services.
|
||||
//
|
||||
// As such, this can really be considered a "metadata service".
|
||||
service Images {
|
||||
// Get returns an image by name.
|
||||
rpc Get(GetRequest) returns (GetResponse);
|
||||
|
||||
// Put assigns the name to a given target image based on the provided
|
||||
// image.
|
||||
rpc Put(PutRequest) returns (google.protobuf.Empty);
|
||||
|
||||
// List returns a list of all images known to containerd.
|
||||
rpc List(ListRequest) returns (ListResponse);
|
||||
|
||||
// Delete deletes the image by name.
|
||||
rpc Delete(DeleteRequest) returns (google.protobuf.Empty);
|
||||
}
|
||||
|
||||
message Image {
|
||||
string name = 1;
|
||||
types.Descriptor target = 2 [(gogoproto.nullable) = false];
|
||||
}
|
||||
|
||||
message GetRequest {
|
||||
string name = 1;
|
||||
|
||||
// TODO(stevvooe): Consider that we may want to have multiple images under
|
||||
// the same name or multiple names for the same image. This mapping could
|
||||
// be truly many to many but we'll need a way to identify an entry.
|
||||
//
|
||||
// For now, we consider it unique but an intermediary index could be
|
||||
// created to allow for a dispatch of images.
|
||||
}
|
||||
|
||||
message GetResponse {
|
||||
Image image = 1;
|
||||
}
|
||||
|
||||
message PutRequest {
|
||||
Image image = 1 [(gogoproto.nullable) = false];
|
||||
}
|
||||
|
||||
message ListRequest {
|
||||
// TODO(stevvooe): empty for now, need to ad filtration
|
||||
// Some common use cases we might consider:
|
||||
//
|
||||
// 1. Select by multiple names.
|
||||
// 2. Select by platform.
|
||||
// 3. Select by annotations.
|
||||
}
|
||||
|
||||
message ListResponse {
|
||||
repeated Image images = 1 [(gogoproto.nullable) = false];
|
||||
|
||||
// TODO(stevvooe): Add pagination.
|
||||
}
|
||||
|
||||
message DeleteRequest {
|
||||
string name = 1;
|
||||
}
|
|
@ -42,7 +42,7 @@ const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package
|
|||
// oci descriptor found in a manifest.
|
||||
// See https://godoc.org/github.com/opencontainers/image-spec/specs-go/v1#Descriptor
|
||||
type Descriptor struct {
|
||||
MediaType string `protobuf:"bytes,1,opt,name=mediaType,proto3" json:"mediaType,omitempty"`
|
||||
MediaType string `protobuf:"bytes,1,opt,name=media_type,json=mediaType,proto3" json:"media_type,omitempty"`
|
||||
Digest github_com_opencontainers_go_digest.Digest `protobuf:"bytes,2,opt,name=digest,proto3,customtype=github.com/opencontainers/go-digest.Digest" json:"digest"`
|
||||
Size_ int64 `protobuf:"varint,3,opt,name=size,proto3" json:"size,omitempty"`
|
||||
}
|
||||
|
@ -403,20 +403,20 @@ func init() {
|
|||
}
|
||||
|
||||
var fileDescriptorDescriptor = []byte{
|
||||
// 225 bytes of a gzipped FileDescriptorProto
|
||||
// 229 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x72, 0x4b, 0xcf, 0x2c, 0xc9,
|
||||
0x28, 0x4d, 0xd2, 0x4b, 0xce, 0xcf, 0xd5, 0x4f, 0xce, 0xcf, 0x2b, 0x49, 0xcc, 0xcc, 0x4b, 0x2d,
|
||||
0x4a, 0x41, 0x66, 0x26, 0x16, 0x64, 0xea, 0x97, 0x54, 0x16, 0xa4, 0x16, 0xeb, 0xa7, 0xa4, 0x16,
|
||||
0x27, 0x17, 0x65, 0x16, 0x94, 0xe4, 0x17, 0x21, 0x31, 0xf5, 0x0a, 0x8a, 0xf2, 0x4b, 0xf2, 0x85,
|
||||
0x84, 0x11, 0x3a, 0xf4, 0xca, 0x0c, 0xf5, 0xc0, 0x1a, 0xa4, 0x44, 0xd2, 0xf3, 0xd3, 0xf3, 0xc1,
|
||||
0xf2, 0xfa, 0x20, 0x16, 0x44, 0xa9, 0x52, 0x17, 0x23, 0x17, 0x97, 0x0b, 0x5c, 0xbf, 0x90, 0x0c,
|
||||
0x17, 0x67, 0x6e, 0x6a, 0x4a, 0x66, 0x62, 0x48, 0x65, 0x41, 0xaa, 0x04, 0xa3, 0x02, 0xa3, 0x06,
|
||||
0x67, 0x10, 0x42, 0x40, 0xc8, 0x8b, 0x8b, 0x2d, 0x25, 0x33, 0x3d, 0xb5, 0xb8, 0x44, 0x82, 0x09,
|
||||
0x24, 0xe5, 0x64, 0x74, 0xe2, 0x9e, 0x3c, 0xc3, 0xad, 0x7b, 0xf2, 0x5a, 0x48, 0xee, 0xce, 0x2f,
|
||||
0x48, 0xcd, 0x83, 0x5b, 0x5f, 0xac, 0x9f, 0x9e, 0xaf, 0x0b, 0xd1, 0xa2, 0xe7, 0x02, 0xa6, 0x82,
|
||||
0xa0, 0x26, 0x08, 0x09, 0x71, 0xb1, 0x14, 0x67, 0x56, 0xa5, 0x4a, 0x30, 0x2b, 0x30, 0x6a, 0x30,
|
||||
0x07, 0x81, 0xd9, 0x4e, 0x12, 0x27, 0x1e, 0xca, 0x31, 0xdc, 0x78, 0x28, 0xc7, 0xd0, 0xf0, 0x48,
|
||||
0x8e, 0xf1, 0xc4, 0x23, 0x39, 0xc6, 0x0b, 0x8f, 0xe4, 0x18, 0x1f, 0x3c, 0x92, 0x63, 0x4c, 0x62,
|
||||
0x03, 0xbb, 0xd6, 0x18, 0x10, 0x00, 0x00, 0xff, 0xff, 0x18, 0xd2, 0x1a, 0xc3, 0x22, 0x01, 0x00,
|
||||
0x00,
|
||||
0xf2, 0xfa, 0x20, 0x16, 0x44, 0xa9, 0x52, 0x37, 0x23, 0x17, 0x97, 0x0b, 0x5c, 0xbf, 0x90, 0x2c,
|
||||
0x17, 0x57, 0x6e, 0x6a, 0x4a, 0x66, 0x62, 0x3c, 0x48, 0x8f, 0x04, 0xa3, 0x02, 0xa3, 0x06, 0x67,
|
||||
0x10, 0x27, 0x58, 0x24, 0xa4, 0xb2, 0x20, 0x55, 0xc8, 0x8b, 0x8b, 0x2d, 0x25, 0x33, 0x3d, 0xb5,
|
||||
0xb8, 0x44, 0x82, 0x09, 0x24, 0xe5, 0x64, 0x74, 0xe2, 0x9e, 0x3c, 0xc3, 0xad, 0x7b, 0xf2, 0x5a,
|
||||
0x48, 0x0e, 0xcf, 0x2f, 0x48, 0xcd, 0x83, 0xdb, 0x5f, 0xac, 0x9f, 0x9e, 0xaf, 0x0b, 0xd1, 0xa2,
|
||||
0xe7, 0x02, 0xa6, 0x82, 0xa0, 0x26, 0x08, 0x09, 0x71, 0xb1, 0x14, 0x67, 0x56, 0xa5, 0x4a, 0x30,
|
||||
0x2b, 0x30, 0x6a, 0x30, 0x07, 0x81, 0xd9, 0x4e, 0x12, 0x27, 0x1e, 0xca, 0x31, 0xdc, 0x78, 0x28,
|
||||
0xc7, 0xd0, 0xf0, 0x48, 0x8e, 0xf1, 0xc4, 0x23, 0x39, 0xc6, 0x0b, 0x8f, 0xe4, 0x18, 0x1f, 0x3c,
|
||||
0x92, 0x63, 0x4c, 0x62, 0x03, 0x3b, 0xd7, 0x18, 0x10, 0x00, 0x00, 0xff, 0xff, 0x45, 0x60, 0xfd,
|
||||
0x5b, 0x23, 0x01, 0x00, 0x00,
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import "gogoproto/gogo.proto";
|
|||
// oci descriptor found in a manifest.
|
||||
// See https://godoc.org/github.com/opencontainers/image-spec/specs-go/v1#Descriptor
|
||||
message Descriptor {
|
||||
string mediaType = 1;
|
||||
string media_type = 1;
|
||||
string digest = 2 [(gogoproto.customtype) = "github.com/opencontainers/go-digest.Digest", (gogoproto.nullable) = false];
|
||||
int64 size = 3;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
_ "github.com/containerd/containerd/services/content"
|
||||
_ "github.com/containerd/containerd/services/execution"
|
||||
_ "github.com/containerd/containerd/services/healthcheck"
|
||||
_ "github.com/containerd/containerd/services/images"
|
||||
_ "github.com/containerd/containerd/services/metrics"
|
||||
_ "github.com/containerd/containerd/services/rootfs"
|
||||
_ "github.com/containerd/containerd/snapshot/btrfs"
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
|
||||
gocontext "golang.org/x/net/context"
|
||||
"google.golang.org/grpc"
|
||||
|
@ -20,8 +21,10 @@ import (
|
|||
"github.com/containerd/containerd"
|
||||
contentapi "github.com/containerd/containerd/api/services/content"
|
||||
api "github.com/containerd/containerd/api/services/execution"
|
||||
imagesapi "github.com/containerd/containerd/api/services/images"
|
||||
rootfsapi "github.com/containerd/containerd/api/services/rootfs"
|
||||
"github.com/containerd/containerd/content"
|
||||
"github.com/containerd/containerd/images"
|
||||
"github.com/containerd/containerd/log"
|
||||
"github.com/containerd/containerd/plugin"
|
||||
"github.com/containerd/containerd/reaper"
|
||||
|
@ -116,11 +119,16 @@ func main() {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
meta, err := resolveMetaDB(context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer meta.Close()
|
||||
snapshotter, err := loadSnapshotter(store)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
services, err := loadServices(runtimes, store, snapshotter)
|
||||
services, err := loadServices(runtimes, store, snapshotter, meta)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -266,6 +274,22 @@ func resolveContentStore() (*content.Store, error) {
|
|||
return content.NewStore(cp)
|
||||
}
|
||||
|
||||
func resolveMetaDB(ctx *cli.Context) (*bolt.DB, error) {
|
||||
path := filepath.Join(conf.Root, "meta.db")
|
||||
|
||||
db, err := bolt.Open(path, 0644, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO(stevvooe): Break these down into components to be initialized.
|
||||
if err := images.InitDB(db); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func loadRuntimes(monitor plugin.ContainerMonitor) (map[string]containerd.Runtime, error) {
|
||||
o := make(map[string]containerd.Runtime)
|
||||
for name, rr := range plugin.Registrations() {
|
||||
|
@ -332,7 +356,7 @@ func loadSnapshotter(store *content.Store) (snapshot.Snapshotter, error) {
|
|||
ic := &plugin.InitContext{
|
||||
Root: conf.Root,
|
||||
State: conf.State,
|
||||
Store: store,
|
||||
Content: store,
|
||||
Context: log.WithModule(global, moduleName),
|
||||
}
|
||||
if sr.Config != nil {
|
||||
|
@ -359,7 +383,7 @@ func newGRPCServer() *grpc.Server {
|
|||
return s
|
||||
}
|
||||
|
||||
func loadServices(runtimes map[string]containerd.Runtime, store *content.Store, sn snapshot.Snapshotter) ([]plugin.Service, error) {
|
||||
func loadServices(runtimes map[string]containerd.Runtime, store *content.Store, sn snapshot.Snapshotter, meta *bolt.DB) ([]plugin.Service, error) {
|
||||
var o []plugin.Service
|
||||
for name, sr := range plugin.Registrations() {
|
||||
if sr.Type != plugin.GRPCPlugin {
|
||||
|
@ -371,7 +395,8 @@ func loadServices(runtimes map[string]containerd.Runtime, store *content.Store,
|
|||
State: conf.State,
|
||||
Context: log.WithModule(global, fmt.Sprintf("service-%s", name)),
|
||||
Runtimes: runtimes,
|
||||
Store: store,
|
||||
Content: store,
|
||||
Meta: meta,
|
||||
Snapshotter: sn,
|
||||
}
|
||||
if sr.Config != nil {
|
||||
|
@ -423,6 +448,8 @@ func interceptor(ctx gocontext.Context,
|
|||
ctx = log.WithModule(ctx, "content")
|
||||
case rootfsapi.RootFSServer:
|
||||
ctx = log.WithModule(ctx, "rootfs")
|
||||
case imagesapi.ImagesServer:
|
||||
ctx = log.WithModule(ctx, "images")
|
||||
default:
|
||||
fmt.Printf("unknown GRPC server type: %#v\n", info.Server)
|
||||
}
|
||||
|
|
|
@ -277,27 +277,18 @@ var runCommand = cli.Command{
|
|||
return err
|
||||
}
|
||||
|
||||
db, err := getDB(context, false)
|
||||
imageStore, err := getImageStore(context)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed opening database")
|
||||
return errors.Wrap(err, "failed resolving image store")
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
tx, err := db.Begin(false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
ref := context.Args().First()
|
||||
|
||||
image, err := images.Get(tx, ref)
|
||||
image, err := imageStore.Get(ctx, ref)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not resolve %q", ref)
|
||||
}
|
||||
// let's close out our db and tx so we don't hold the lock whilst running.
|
||||
tx.Rollback()
|
||||
db.Close()
|
||||
|
||||
diffIDs, err := image.RootFS(ctx, provider)
|
||||
if err != nil {
|
||||
|
|
|
@ -14,14 +14,15 @@ import (
|
|||
|
||||
gocontext "context"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
contentapi "github.com/containerd/containerd/api/services/content"
|
||||
"github.com/containerd/containerd/api/services/execution"
|
||||
imagesapi "github.com/containerd/containerd/api/services/images"
|
||||
rootfsapi "github.com/containerd/containerd/api/services/rootfs"
|
||||
"github.com/containerd/containerd/api/types/container"
|
||||
"github.com/containerd/containerd/content"
|
||||
"github.com/containerd/containerd/images"
|
||||
contentservice "github.com/containerd/containerd/services/content"
|
||||
imagesservice "github.com/containerd/containerd/services/images"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/tonistiigi/fifo"
|
||||
"github.com/urfave/cli"
|
||||
|
@ -134,25 +135,12 @@ func getRootFSService(context *cli.Context) (rootfsapi.RootFSClient, error) {
|
|||
return rootfsapi.NewRootFSClient(conn), nil
|
||||
}
|
||||
|
||||
func getDB(ctx *cli.Context, readonly bool) (*bolt.DB, error) {
|
||||
// TODO(stevvooe): For now, we operate directly on the database. We will
|
||||
// replace this with a GRPC service when the details are more concrete.
|
||||
path := filepath.Join(ctx.GlobalString("root"), "meta.db")
|
||||
|
||||
db, err := bolt.Open(path, 0644, &bolt.Options{
|
||||
ReadOnly: readonly,
|
||||
})
|
||||
func getImageStore(clicontext *cli.Context) (images.Store, error) {
|
||||
conn, err := getGRPCConnection(clicontext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !readonly {
|
||||
if err := images.InitDB(db); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return db, nil
|
||||
return imagesservice.NewStoreFromClient(imagesapi.NewImagesClient(conn)), nil
|
||||
}
|
||||
|
||||
func getTempDir(id string) (string, error) {
|
||||
|
|
32
cmd/dist/common.go
vendored
32
cmd/dist/common.go
vendored
|
@ -6,11 +6,12 @@ import (
|
|||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
imagesapi "github.com/containerd/containerd/api/services/images"
|
||||
"github.com/containerd/containerd/content"
|
||||
"github.com/containerd/containerd/images"
|
||||
"github.com/containerd/containerd/remotes"
|
||||
"github.com/containerd/containerd/remotes/docker"
|
||||
imagesservice "github.com/containerd/containerd/services/images"
|
||||
"github.com/urfave/cli"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
@ -27,6 +28,14 @@ func resolveContentStore(context *cli.Context) (*content.Store, error) {
|
|||
return content.NewStore(root)
|
||||
}
|
||||
|
||||
func resolveImageStore(clicontext *cli.Context) (images.Store, error) {
|
||||
conn, err := connectGRPC(clicontext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return imagesservice.NewStoreFromClient(imagesapi.NewImagesClient(conn)), nil
|
||||
}
|
||||
|
||||
func connectGRPC(context *cli.Context) (*grpc.ClientConn, error) {
|
||||
socket := context.GlobalString("socket")
|
||||
timeout := context.GlobalDuration("connect-timeout")
|
||||
|
@ -40,27 +49,6 @@ func connectGRPC(context *cli.Context) (*grpc.ClientConn, error) {
|
|||
)
|
||||
}
|
||||
|
||||
func getDB(ctx *cli.Context, readonly bool) (*bolt.DB, error) {
|
||||
// TODO(stevvooe): For now, we operate directly on the database. We will
|
||||
// replace this with a GRPC service when the details are more concrete.
|
||||
path := filepath.Join(ctx.GlobalString("root"), "meta.db")
|
||||
|
||||
db, err := bolt.Open(path, 0644, &bolt.Options{
|
||||
ReadOnly: readonly,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !readonly {
|
||||
if err := images.InitDB(db); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// getResolver prepares the resolver from the environment and options.
|
||||
func getResolver(ctx context.Context) (remotes.Resolver, error) {
|
||||
return docker.NewResolver(), nil
|
||||
|
|
27
cmd/dist/images.go
vendored
27
cmd/dist/images.go
vendored
|
@ -6,7 +6,6 @@ import (
|
|||
"text/tabwriter"
|
||||
|
||||
contentapi "github.com/containerd/containerd/api/services/content"
|
||||
"github.com/containerd/containerd/images"
|
||||
"github.com/containerd/containerd/log"
|
||||
"github.com/containerd/containerd/progress"
|
||||
contentservice "github.com/containerd/containerd/services/content"
|
||||
|
@ -25,23 +24,19 @@ var imagesCommand = cli.Command{
|
|||
ctx = background
|
||||
)
|
||||
|
||||
db, err := getDB(clicontext, true)
|
||||
imageStore, err := resolveImageStore(clicontext)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to open database")
|
||||
return err
|
||||
}
|
||||
tx, err := db.Begin(false)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not start transaction")
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
conn, err := connectGRPC(clicontext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
provider := contentservice.NewProviderFromClient(contentapi.NewContentClient(conn))
|
||||
|
||||
images, err := images.List(tx)
|
||||
images, err := imageStore.List(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to list images")
|
||||
}
|
||||
|
@ -54,7 +49,7 @@ var imagesCommand = cli.Command{
|
|||
log.G(ctx).WithError(err).Errorf("failed calculating size for image %s", image.Name)
|
||||
}
|
||||
|
||||
fmt.Fprintf(tw, "%v\t%v\t%v\t%v\t\n", image.Name, image.Descriptor.MediaType, image.Descriptor.Digest, progress.Bytes(size))
|
||||
fmt.Fprintf(tw, "%v\t%v\t%v\t%v\t\n", image.Name, image.Target.MediaType, image.Target.Digest, progress.Bytes(size))
|
||||
}
|
||||
tw.Flush()
|
||||
|
||||
|
@ -74,19 +69,13 @@ var rmiCommand = cli.Command{
|
|||
exitErr error
|
||||
)
|
||||
|
||||
db, err := getDB(clicontext, false)
|
||||
imageStore, err := resolveImageStore(clicontext)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to open database")
|
||||
return err
|
||||
}
|
||||
|
||||
tx, err := db.Begin(true)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not start transaction")
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
for _, target := range clicontext.Args() {
|
||||
if err := images.Delete(tx, target); err != nil {
|
||||
if err := imageStore.Delete(ctx, target); err != nil {
|
||||
if exitErr == nil {
|
||||
exitErr = errors.Wrapf(err, "unable to delete %v", target)
|
||||
}
|
||||
|
|
27
cmd/dist/pull.go
vendored
27
cmd/dist/pull.go
vendored
|
@ -47,17 +47,10 @@ command. As part of this process, we do the following:
|
|||
return err
|
||||
}
|
||||
|
||||
db, err := getDB(clicontext, false)
|
||||
imageStore, err := resolveImageStore(clicontext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
tx, err := db.Begin(true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
resolver, err := getResolver(ctx)
|
||||
if err != nil {
|
||||
|
@ -65,6 +58,7 @@ command. As part of this process, we do the following:
|
|||
}
|
||||
ongoing := newJobs()
|
||||
|
||||
// TODO(stevvooe): Must unify this type.
|
||||
ingester := contentservice.NewIngesterFromClient(contentapi.NewContentClient(conn))
|
||||
provider := contentservice.NewProviderFromClient(contentapi.NewContentClient(conn))
|
||||
|
||||
|
@ -88,13 +82,8 @@ command. As part of this process, we do the following:
|
|||
close(resolved)
|
||||
|
||||
eg.Go(func() error {
|
||||
return images.Register(tx, name, desc)
|
||||
return imageStore.Put(ctx, name, desc)
|
||||
})
|
||||
defer func() {
|
||||
if err := tx.Commit(); err != nil {
|
||||
log.G(ctx).WithError(err).Error("commit failed")
|
||||
}
|
||||
}()
|
||||
|
||||
return images.Dispatch(ctx,
|
||||
images.Handlers(images.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
|
||||
|
@ -114,24 +103,20 @@ command. As part of this process, we do the following:
|
|||
}()
|
||||
|
||||
defer func() {
|
||||
ctx := context.Background()
|
||||
tx, err := db.Begin(false)
|
||||
if err != nil {
|
||||
log.G(ctx).Fatal(err)
|
||||
}
|
||||
ctx := background
|
||||
|
||||
// TODO(stevvooe): This section unpacks the layers and resolves the
|
||||
// root filesystem chainid for the image. For now, we just print
|
||||
// it, but we should keep track of this in the metadata storage.
|
||||
|
||||
image, err := images.Get(tx, resolvedImageName)
|
||||
image, err := imageStore.Get(ctx, resolvedImageName)
|
||||
if err != nil {
|
||||
log.G(ctx).Fatal(err)
|
||||
}
|
||||
|
||||
provider := contentservice.NewProviderFromClient(contentapi.NewContentClient(conn))
|
||||
|
||||
p, err := content.ReadBlob(ctx, provider, image.Descriptor.Digest)
|
||||
p, err := content.ReadBlob(ctx, provider, image.Target.Digest)
|
||||
if err != nil {
|
||||
log.G(ctx).Fatal(err)
|
||||
}
|
||||
|
|
|
@ -14,8 +14,8 @@ import (
|
|||
|
||||
// Image provides the model for how containerd views container images.
|
||||
type Image struct {
|
||||
Name string
|
||||
Descriptor ocispec.Descriptor
|
||||
Name string
|
||||
Target ocispec.Descriptor
|
||||
}
|
||||
|
||||
// TODO(stevvooe): Many of these functions make strong platform assumptions,
|
||||
|
@ -29,9 +29,9 @@ type Image struct {
|
|||
func (image *Image) Config(ctx context.Context, provider content.Provider) (ocispec.Descriptor, error) {
|
||||
var configDesc ocispec.Descriptor
|
||||
return configDesc, Walk(ctx, HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
|
||||
switch image.Descriptor.MediaType {
|
||||
switch image.Target.MediaType {
|
||||
case MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest:
|
||||
rc, err := provider.Reader(ctx, image.Descriptor.Digest)
|
||||
rc, err := provider.Reader(ctx, image.Target.Digest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ func (image *Image) Config(ctx context.Context, provider content.Provider) (ocis
|
|||
return nil, errors.New("could not resolve config")
|
||||
}
|
||||
|
||||
}), image.Descriptor)
|
||||
}), image.Target)
|
||||
}
|
||||
|
||||
// RootFS returns the unpacked diffids that make up and images rootfs.
|
||||
|
@ -91,10 +91,10 @@ func (image *Image) RootFS(ctx context.Context, provider content.Provider) ([]di
|
|||
func (image *Image) Size(ctx context.Context, provider content.Provider) (int64, error) {
|
||||
var size int64
|
||||
return size, Walk(ctx, HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
|
||||
switch image.Descriptor.MediaType {
|
||||
switch image.Target.MediaType {
|
||||
case MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest:
|
||||
size += desc.Size
|
||||
rc, err := provider.Reader(ctx, image.Descriptor.Digest)
|
||||
rc, err := provider.Reader(ctx, image.Target.Digest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -121,5 +121,5 @@ func (image *Image) Size(ctx context.Context, provider content.Provider) (int64,
|
|||
return nil, errors.New("unsupported type")
|
||||
}
|
||||
|
||||
}), image.Descriptor)
|
||||
}), image.Target)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package images
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
|
@ -8,12 +9,30 @@ import (
|
|||
"github.com/containerd/containerd/log"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
errImageUnknown = fmt.Errorf("image: unknown")
|
||||
ErrExists = errors.New("images: exists")
|
||||
ErrNotFound = errors.New("images: not found")
|
||||
)
|
||||
|
||||
type Store interface {
|
||||
Put(ctx context.Context, name string, desc ocispec.Descriptor) error
|
||||
Get(ctx context.Context, name string) (Image, error)
|
||||
List(ctx context.Context) ([]Image, error)
|
||||
Delete(ctx context.Context, name string) error
|
||||
}
|
||||
|
||||
// IsNotFound returns true if the error is due to a missing image.
|
||||
func IsNotFound(err error) bool {
|
||||
return errors.Cause(err) == ErrNotFound
|
||||
}
|
||||
|
||||
func IsExists(err error) bool {
|
||||
return errors.Cause(err) == ErrExists
|
||||
}
|
||||
|
||||
var (
|
||||
bucketKeyStorageVersion = []byte("v1")
|
||||
bucketKeyImages = []byte("images")
|
||||
|
@ -26,6 +45,8 @@ var (
|
|||
// "metadata" store. For now, it is bound tightly to the local machine and bolt
|
||||
// but we can take this and use it to define a service interface.
|
||||
|
||||
// InitDB will initialize the database for use. The database must be opened for
|
||||
// write and the caller must not be holding an open transaction.
|
||||
func InitDB(db *bolt.DB) error {
|
||||
log.L.Debug("init db")
|
||||
return db.Update(func(tx *bolt.Tx) error {
|
||||
|
@ -34,8 +55,28 @@ func InitDB(db *bolt.DB) error {
|
|||
})
|
||||
}
|
||||
|
||||
func Register(tx *bolt.Tx, name string, desc ocispec.Descriptor) error {
|
||||
return withImagesBucket(tx, func(bkt *bolt.Bucket) error {
|
||||
func NewImageStore(tx *bolt.Tx) Store {
|
||||
return &storage{tx: tx}
|
||||
}
|
||||
|
||||
type storage struct {
|
||||
tx *bolt.Tx
|
||||
}
|
||||
|
||||
func (s *storage) Get(ctx context.Context, name string) (Image, error) {
|
||||
var image Image
|
||||
if err := withImageBucket(s.tx, name, func(bkt *bolt.Bucket) error {
|
||||
image.Name = name
|
||||
return readImage(&image, bkt)
|
||||
}); err != nil {
|
||||
return Image{}, err
|
||||
}
|
||||
|
||||
return image, nil
|
||||
}
|
||||
|
||||
func (s *storage) Put(ctx context.Context, name string, desc ocispec.Descriptor) error {
|
||||
return withImagesBucket(s.tx, func(bkt *bolt.Bucket) error {
|
||||
ibkt, err := bkt.CreateBucketIfNotExists([]byte(name))
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -65,22 +106,10 @@ func Register(tx *bolt.Tx, name string, desc ocispec.Descriptor) error {
|
|||
})
|
||||
}
|
||||
|
||||
func Get(tx *bolt.Tx, name string) (Image, error) {
|
||||
var image Image
|
||||
if err := withImageBucket(tx, name, func(bkt *bolt.Bucket) error {
|
||||
image.Name = name
|
||||
return readImage(&image, bkt)
|
||||
}); err != nil {
|
||||
return Image{}, err
|
||||
}
|
||||
|
||||
return image, nil
|
||||
}
|
||||
|
||||
func List(tx *bolt.Tx) ([]Image, error) {
|
||||
func (s *storage) List(ctx context.Context) ([]Image, error) {
|
||||
var images []Image
|
||||
|
||||
if err := withImagesBucket(tx, func(bkt *bolt.Bucket) error {
|
||||
if err := withImagesBucket(s.tx, func(bkt *bolt.Bucket) error {
|
||||
return bkt.ForEach(func(k, v []byte) error {
|
||||
var (
|
||||
image = Image{
|
||||
|
@ -103,8 +132,8 @@ func List(tx *bolt.Tx) ([]Image, error) {
|
|||
return images, nil
|
||||
}
|
||||
|
||||
func Delete(tx *bolt.Tx, name string) error {
|
||||
return withImagesBucket(tx, func(bkt *bolt.Bucket) error {
|
||||
func (s *storage) Delete(ctx context.Context, name string) error {
|
||||
return withImagesBucket(s.tx, func(bkt *bolt.Bucket) error {
|
||||
return bkt.DeleteBucket([]byte(name))
|
||||
})
|
||||
}
|
||||
|
@ -119,11 +148,11 @@ func readImage(image *Image, bkt *bolt.Bucket) error {
|
|||
// keys, rather than full arrays.
|
||||
switch string(k) {
|
||||
case string(bucketKeyDigest):
|
||||
image.Descriptor.Digest = digest.Digest(v)
|
||||
image.Target.Digest = digest.Digest(v)
|
||||
case string(bucketKeyMediaType):
|
||||
image.Descriptor.MediaType = string(v)
|
||||
image.Target.MediaType = string(v)
|
||||
case string(bucketKeySize):
|
||||
image.Descriptor.Size, _ = binary.Varint(v)
|
||||
image.Target.Size, _ = binary.Varint(v)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -149,7 +178,7 @@ func createBucketIfNotExists(tx *bolt.Tx, keys ...[]byte) (*bolt.Bucket, error)
|
|||
func withImagesBucket(tx *bolt.Tx, fn func(bkt *bolt.Bucket) error) error {
|
||||
bkt := getImagesBucket(tx)
|
||||
if bkt == nil {
|
||||
return errImageUnknown
|
||||
return ErrNotFound
|
||||
}
|
||||
|
||||
return fn(bkt)
|
||||
|
@ -158,7 +187,7 @@ func withImagesBucket(tx *bolt.Tx, fn func(bkt *bolt.Bucket) error) error {
|
|||
func withImageBucket(tx *bolt.Tx, name string, fn func(bkt *bolt.Bucket) error) error {
|
||||
bkt := getImageBucket(tx, name)
|
||||
if bkt == nil {
|
||||
return errImageUnknown
|
||||
return ErrNotFound
|
||||
}
|
||||
|
||||
return fn(bkt)
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/containerd/containerd"
|
||||
"github.com/containerd/containerd/content"
|
||||
"github.com/containerd/containerd/snapshot"
|
||||
|
@ -32,7 +33,8 @@ type InitContext struct {
|
|||
Root string
|
||||
State string
|
||||
Runtimes map[string]containerd.Runtime
|
||||
Store *content.Store
|
||||
Content *content.Store
|
||||
Meta *bolt.DB
|
||||
Snapshotter snapshot.Snapshotter
|
||||
Config interface{}
|
||||
Context context.Context
|
||||
|
|
|
@ -38,7 +38,7 @@ func init() {
|
|||
|
||||
func NewService(ic *plugin.InitContext) (interface{}, error) {
|
||||
return &Service{
|
||||
store: ic.Store,
|
||||
store: ic.Content,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
60
services/images/client.go
Normal file
60
services/images/client.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
package images
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
imagesapi "github.com/containerd/containerd/api/services/images"
|
||||
"github.com/containerd/containerd/images"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
type remoteStore struct {
|
||||
client imagesapi.ImagesClient
|
||||
}
|
||||
|
||||
func NewStoreFromClient(client imagesapi.ImagesClient) images.Store {
|
||||
return &remoteStore{
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *remoteStore) Put(ctx context.Context, name string, desc ocispec.Descriptor) error {
|
||||
// TODO(stevvooe): Consider that the remote may want to augment and return
|
||||
// a modified image.
|
||||
_, err := s.client.Put(ctx, &imagesapi.PutRequest{
|
||||
Image: imagesapi.Image{
|
||||
Name: name,
|
||||
Target: descToProto(&desc),
|
||||
},
|
||||
})
|
||||
|
||||
return rewriteGRPCError(err)
|
||||
}
|
||||
|
||||
func (s *remoteStore) Get(ctx context.Context, name string) (images.Image, error) {
|
||||
resp, err := s.client.Get(ctx, &imagesapi.GetRequest{
|
||||
Name: name,
|
||||
})
|
||||
if err != nil {
|
||||
return images.Image{}, rewriteGRPCError(err)
|
||||
}
|
||||
|
||||
return imageFromProto(resp.Image), nil
|
||||
}
|
||||
|
||||
func (s *remoteStore) List(ctx context.Context) ([]images.Image, error) {
|
||||
resp, err := s.client.List(ctx, &imagesapi.ListRequest{})
|
||||
if err != nil {
|
||||
return nil, rewriteGRPCError(err)
|
||||
}
|
||||
|
||||
return imagesFromProto(resp.Images), nil
|
||||
}
|
||||
|
||||
func (s *remoteStore) Delete(ctx context.Context, name string) error {
|
||||
_, err := s.client.Delete(ctx, &imagesapi.DeleteRequest{
|
||||
Name: name,
|
||||
})
|
||||
|
||||
return rewriteGRPCError(err)
|
||||
}
|
87
services/images/helpers.go
Normal file
87
services/images/helpers.go
Normal file
|
@ -0,0 +1,87 @@
|
|||
package images
|
||||
|
||||
import (
|
||||
imagesapi "github.com/containerd/containerd/api/services/images"
|
||||
"github.com/containerd/containerd/api/types/descriptor"
|
||||
"github.com/containerd/containerd/images"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
)
|
||||
|
||||
func imagesToProto(images []images.Image) []imagesapi.Image {
|
||||
var imagespb []imagesapi.Image
|
||||
|
||||
for _, image := range images {
|
||||
imagespb = append(imagespb, imageToProto(&image))
|
||||
}
|
||||
|
||||
return imagespb
|
||||
}
|
||||
|
||||
func imagesFromProto(imagespb []imagesapi.Image) []images.Image {
|
||||
var images []images.Image
|
||||
|
||||
for _, image := range imagespb {
|
||||
images = append(images, imageFromProto(&image))
|
||||
}
|
||||
|
||||
return images
|
||||
}
|
||||
|
||||
func imageToProto(image *images.Image) imagesapi.Image {
|
||||
return imagesapi.Image{
|
||||
Name: image.Name,
|
||||
Target: descToProto(&image.Target),
|
||||
}
|
||||
}
|
||||
|
||||
func imageFromProto(imagepb *imagesapi.Image) images.Image {
|
||||
return images.Image{
|
||||
Name: imagepb.Name,
|
||||
Target: descFromProto(&imagepb.Target),
|
||||
}
|
||||
}
|
||||
|
||||
func descFromProto(desc *descriptor.Descriptor) ocispec.Descriptor {
|
||||
return ocispec.Descriptor{
|
||||
MediaType: desc.MediaType,
|
||||
Size: desc.Size_,
|
||||
Digest: desc.Digest,
|
||||
}
|
||||
}
|
||||
|
||||
func descToProto(desc *ocispec.Descriptor) descriptor.Descriptor {
|
||||
return descriptor.Descriptor{
|
||||
MediaType: desc.MediaType,
|
||||
Size_: desc.Size,
|
||||
Digest: desc.Digest,
|
||||
}
|
||||
}
|
||||
|
||||
func rewriteGRPCError(err error) error {
|
||||
if err == nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch grpc.Code(errors.Cause(err)) {
|
||||
case codes.AlreadyExists:
|
||||
return images.ErrExists
|
||||
case codes.NotFound:
|
||||
return images.ErrNotFound
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func mapGRPCError(err error, id string) error {
|
||||
switch {
|
||||
case images.IsNotFound(err):
|
||||
return grpc.Errorf(codes.NotFound, "image %v not found", id)
|
||||
case images.IsExists(err):
|
||||
return grpc.Errorf(codes.AlreadyExists, "image %v already exists", id)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
91
services/images/service.go
Normal file
91
services/images/service.go
Normal file
|
@ -0,0 +1,91 @@
|
|||
package images
|
||||
|
||||
import (
|
||||
"github.com/boltdb/bolt"
|
||||
imagesapi "github.com/containerd/containerd/api/services/images"
|
||||
"github.com/containerd/containerd/images"
|
||||
"github.com/containerd/containerd/plugin"
|
||||
"github.com/golang/protobuf/ptypes/empty"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func init() {
|
||||
plugin.Register("images-grpc", &plugin.Registration{
|
||||
Type: plugin.GRPCPlugin,
|
||||
Init: func(ic *plugin.InitContext) (interface{}, error) {
|
||||
return NewService(ic.Meta), nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
db *bolt.DB
|
||||
}
|
||||
|
||||
func NewService(db *bolt.DB) imagesapi.ImagesServer {
|
||||
return &Service{db: db}
|
||||
}
|
||||
|
||||
func (s *Service) Register(server *grpc.Server) error {
|
||||
imagesapi.RegisterImagesServer(server, s)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) Get(ctx context.Context, req *imagesapi.GetRequest) (*imagesapi.GetResponse, error) {
|
||||
var resp imagesapi.GetResponse
|
||||
|
||||
return &resp, s.withStoreTx(ctx, req.Name, false, func(ctx context.Context, store images.Store) error {
|
||||
image, err := store.Get(ctx, req.Name)
|
||||
if err != nil {
|
||||
return mapGRPCError(err, req.Name)
|
||||
}
|
||||
imagepb := imageToProto(&image)
|
||||
resp.Image = &imagepb
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) Put(ctx context.Context, req *imagesapi.PutRequest) (*empty.Empty, error) {
|
||||
return &empty.Empty{}, s.withStoreTx(ctx, req.Image.Name, true, func(ctx context.Context, store images.Store) error {
|
||||
return mapGRPCError(store.Put(ctx, req.Image.Name, descFromProto(&req.Image.Target)), req.Image.Name)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) List(ctx context.Context, _ *imagesapi.ListRequest) (*imagesapi.ListResponse, error) {
|
||||
var resp imagesapi.ListResponse
|
||||
|
||||
return &resp, s.withStoreTx(ctx, "", false, func(ctx context.Context, store images.Store) error {
|
||||
images, err := store.List(ctx)
|
||||
if err != nil {
|
||||
return mapGRPCError(err, "")
|
||||
}
|
||||
|
||||
resp.Images = imagesToProto(images)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) Delete(ctx context.Context, req *imagesapi.DeleteRequest) (*empty.Empty, error) {
|
||||
return &empty.Empty{}, s.withStoreTx(ctx, req.Name, true, func(ctx context.Context, store images.Store) error {
|
||||
return mapGRPCError(store.Delete(ctx, req.Name), req.Name)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) withStoreTx(ctx context.Context, id string, writable bool, fn func(ctx context.Context, store images.Store) error) error {
|
||||
tx, err := s.db.Begin(writable)
|
||||
if err != nil {
|
||||
return mapGRPCError(err, id)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
if err := fn(ctx, images.NewImageStore(tx)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if writable {
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -22,7 +22,7 @@ func init() {
|
|||
plugin.Register("rootfs-grpc", &plugin.Registration{
|
||||
Type: plugin.GRPCPlugin,
|
||||
Init: func(ic *plugin.InitContext) (interface{}, error) {
|
||||
return NewService(ic.Store, ic.Snapshotter)
|
||||
return NewService(ic.Content, ic.Snapshotter)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue