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.
|
// oci descriptor found in a manifest.
|
||||||
// See https://godoc.org/github.com/opencontainers/image-spec/specs-go/v1#Descriptor
|
// See https://godoc.org/github.com/opencontainers/image-spec/specs-go/v1#Descriptor
|
||||||
type Descriptor struct {
|
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"`
|
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"`
|
Size_ int64 `protobuf:"varint,3,opt,name=size,proto3" json:"size,omitempty"`
|
||||||
}
|
}
|
||||||
|
@ -403,20 +403,20 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileDescriptorDescriptor = []byte{
|
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,
|
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,
|
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,
|
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,
|
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,
|
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,
|
0xf2, 0xfa, 0x20, 0x16, 0x44, 0xa9, 0x52, 0x37, 0x23, 0x17, 0x97, 0x0b, 0x5c, 0xbf, 0x90, 0x2c,
|
||||||
0x17, 0x67, 0x6e, 0x6a, 0x4a, 0x66, 0x62, 0x48, 0x65, 0x41, 0xaa, 0x04, 0xa3, 0x02, 0xa3, 0x06,
|
0x17, 0x57, 0x6e, 0x6a, 0x4a, 0x66, 0x62, 0x3c, 0x48, 0x8f, 0x04, 0xa3, 0x02, 0xa3, 0x06, 0x67,
|
||||||
0x67, 0x10, 0x42, 0x40, 0xc8, 0x8b, 0x8b, 0x2d, 0x25, 0x33, 0x3d, 0xb5, 0xb8, 0x44, 0x82, 0x09,
|
0x10, 0x27, 0x58, 0x24, 0xa4, 0xb2, 0x20, 0x55, 0xc8, 0x8b, 0x8b, 0x2d, 0x25, 0x33, 0x3d, 0xb5,
|
||||||
0x24, 0xe5, 0x64, 0x74, 0xe2, 0x9e, 0x3c, 0xc3, 0xad, 0x7b, 0xf2, 0x5a, 0x48, 0xee, 0xce, 0x2f,
|
0xb8, 0x44, 0x82, 0x09, 0x24, 0xe5, 0x64, 0x74, 0xe2, 0x9e, 0x3c, 0xc3, 0xad, 0x7b, 0xf2, 0x5a,
|
||||||
0x48, 0xcd, 0x83, 0x5b, 0x5f, 0xac, 0x9f, 0x9e, 0xaf, 0x0b, 0xd1, 0xa2, 0xe7, 0x02, 0xa6, 0x82,
|
0x48, 0x0e, 0xcf, 0x2f, 0x48, 0xcd, 0x83, 0xdb, 0x5f, 0xac, 0x9f, 0x9e, 0xaf, 0x0b, 0xd1, 0xa2,
|
||||||
0xa0, 0x26, 0x08, 0x09, 0x71, 0xb1, 0x14, 0x67, 0x56, 0xa5, 0x4a, 0x30, 0x2b, 0x30, 0x6a, 0x30,
|
0xe7, 0x02, 0xa6, 0x82, 0xa0, 0x26, 0x08, 0x09, 0x71, 0xb1, 0x14, 0x67, 0x56, 0xa5, 0x4a, 0x30,
|
||||||
0x07, 0x81, 0xd9, 0x4e, 0x12, 0x27, 0x1e, 0xca, 0x31, 0xdc, 0x78, 0x28, 0xc7, 0xd0, 0xf0, 0x48,
|
0x2b, 0x30, 0x6a, 0x30, 0x07, 0x81, 0xd9, 0x4e, 0x12, 0x27, 0x1e, 0xca, 0x31, 0xdc, 0x78, 0x28,
|
||||||
0x8e, 0xf1, 0xc4, 0x23, 0x39, 0xc6, 0x0b, 0x8f, 0xe4, 0x18, 0x1f, 0x3c, 0x92, 0x63, 0x4c, 0x62,
|
0xc7, 0xd0, 0xf0, 0x48, 0x8e, 0xf1, 0xc4, 0x23, 0x39, 0xc6, 0x0b, 0x8f, 0xe4, 0x18, 0x1f, 0x3c,
|
||||||
0x03, 0xbb, 0xd6, 0x18, 0x10, 0x00, 0x00, 0xff, 0xff, 0x18, 0xd2, 0x1a, 0xc3, 0x22, 0x01, 0x00,
|
0x92, 0x63, 0x4c, 0x62, 0x03, 0x3b, 0xd7, 0x18, 0x10, 0x00, 0x00, 0xff, 0xff, 0x45, 0x60, 0xfd,
|
||||||
0x00,
|
0x5b, 0x23, 0x01, 0x00, 0x00,
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import "gogoproto/gogo.proto";
|
||||||
// oci descriptor found in a manifest.
|
// oci descriptor found in a manifest.
|
||||||
// See https://godoc.org/github.com/opencontainers/image-spec/specs-go/v1#Descriptor
|
// See https://godoc.org/github.com/opencontainers/image-spec/specs-go/v1#Descriptor
|
||||||
message Descriptor {
|
message Descriptor {
|
||||||
string mediaType = 1;
|
string media_type = 1;
|
||||||
string digest = 2 [(gogoproto.customtype) = "github.com/opencontainers/go-digest.Digest", (gogoproto.nullable) = false];
|
string digest = 2 [(gogoproto.customtype) = "github.com/opencontainers/go-digest.Digest", (gogoproto.nullable) = false];
|
||||||
int64 size = 3;
|
int64 size = 3;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
_ "github.com/containerd/containerd/services/content"
|
_ "github.com/containerd/containerd/services/content"
|
||||||
_ "github.com/containerd/containerd/services/execution"
|
_ "github.com/containerd/containerd/services/execution"
|
||||||
_ "github.com/containerd/containerd/services/healthcheck"
|
_ "github.com/containerd/containerd/services/healthcheck"
|
||||||
|
_ "github.com/containerd/containerd/services/images"
|
||||||
_ "github.com/containerd/containerd/services/metrics"
|
_ "github.com/containerd/containerd/services/metrics"
|
||||||
_ "github.com/containerd/containerd/services/rootfs"
|
_ "github.com/containerd/containerd/services/rootfs"
|
||||||
_ "github.com/containerd/containerd/snapshot/btrfs"
|
_ "github.com/containerd/containerd/snapshot/btrfs"
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/boltdb/bolt"
|
||||||
grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
|
grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
|
||||||
gocontext "golang.org/x/net/context"
|
gocontext "golang.org/x/net/context"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
|
@ -20,8 +21,10 @@ import (
|
||||||
"github.com/containerd/containerd"
|
"github.com/containerd/containerd"
|
||||||
contentapi "github.com/containerd/containerd/api/services/content"
|
contentapi "github.com/containerd/containerd/api/services/content"
|
||||||
api "github.com/containerd/containerd/api/services/execution"
|
api "github.com/containerd/containerd/api/services/execution"
|
||||||
|
imagesapi "github.com/containerd/containerd/api/services/images"
|
||||||
rootfsapi "github.com/containerd/containerd/api/services/rootfs"
|
rootfsapi "github.com/containerd/containerd/api/services/rootfs"
|
||||||
"github.com/containerd/containerd/content"
|
"github.com/containerd/containerd/content"
|
||||||
|
"github.com/containerd/containerd/images"
|
||||||
"github.com/containerd/containerd/log"
|
"github.com/containerd/containerd/log"
|
||||||
"github.com/containerd/containerd/plugin"
|
"github.com/containerd/containerd/plugin"
|
||||||
"github.com/containerd/containerd/reaper"
|
"github.com/containerd/containerd/reaper"
|
||||||
|
@ -116,11 +119,16 @@ func main() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
meta, err := resolveMetaDB(context)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer meta.Close()
|
||||||
snapshotter, err := loadSnapshotter(store)
|
snapshotter, err := loadSnapshotter(store)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
services, err := loadServices(runtimes, store, snapshotter)
|
services, err := loadServices(runtimes, store, snapshotter, meta)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -266,6 +274,22 @@ func resolveContentStore() (*content.Store, error) {
|
||||||
return content.NewStore(cp)
|
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) {
|
func loadRuntimes(monitor plugin.ContainerMonitor) (map[string]containerd.Runtime, error) {
|
||||||
o := make(map[string]containerd.Runtime)
|
o := make(map[string]containerd.Runtime)
|
||||||
for name, rr := range plugin.Registrations() {
|
for name, rr := range plugin.Registrations() {
|
||||||
|
@ -332,7 +356,7 @@ func loadSnapshotter(store *content.Store) (snapshot.Snapshotter, error) {
|
||||||
ic := &plugin.InitContext{
|
ic := &plugin.InitContext{
|
||||||
Root: conf.Root,
|
Root: conf.Root,
|
||||||
State: conf.State,
|
State: conf.State,
|
||||||
Store: store,
|
Content: store,
|
||||||
Context: log.WithModule(global, moduleName),
|
Context: log.WithModule(global, moduleName),
|
||||||
}
|
}
|
||||||
if sr.Config != nil {
|
if sr.Config != nil {
|
||||||
|
@ -359,7 +383,7 @@ func newGRPCServer() *grpc.Server {
|
||||||
return s
|
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
|
var o []plugin.Service
|
||||||
for name, sr := range plugin.Registrations() {
|
for name, sr := range plugin.Registrations() {
|
||||||
if sr.Type != plugin.GRPCPlugin {
|
if sr.Type != plugin.GRPCPlugin {
|
||||||
|
@ -371,7 +395,8 @@ func loadServices(runtimes map[string]containerd.Runtime, store *content.Store,
|
||||||
State: conf.State,
|
State: conf.State,
|
||||||
Context: log.WithModule(global, fmt.Sprintf("service-%s", name)),
|
Context: log.WithModule(global, fmt.Sprintf("service-%s", name)),
|
||||||
Runtimes: runtimes,
|
Runtimes: runtimes,
|
||||||
Store: store,
|
Content: store,
|
||||||
|
Meta: meta,
|
||||||
Snapshotter: sn,
|
Snapshotter: sn,
|
||||||
}
|
}
|
||||||
if sr.Config != nil {
|
if sr.Config != nil {
|
||||||
|
@ -423,6 +448,8 @@ func interceptor(ctx gocontext.Context,
|
||||||
ctx = log.WithModule(ctx, "content")
|
ctx = log.WithModule(ctx, "content")
|
||||||
case rootfsapi.RootFSServer:
|
case rootfsapi.RootFSServer:
|
||||||
ctx = log.WithModule(ctx, "rootfs")
|
ctx = log.WithModule(ctx, "rootfs")
|
||||||
|
case imagesapi.ImagesServer:
|
||||||
|
ctx = log.WithModule(ctx, "images")
|
||||||
default:
|
default:
|
||||||
fmt.Printf("unknown GRPC server type: %#v\n", info.Server)
|
fmt.Printf("unknown GRPC server type: %#v\n", info.Server)
|
||||||
}
|
}
|
||||||
|
|
|
@ -277,27 +277,18 @@ var runCommand = cli.Command{
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := getDB(context, false)
|
imageStore, err := getImageStore(context)
|
||||||
if err != nil {
|
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()
|
ref := context.Args().First()
|
||||||
|
|
||||||
image, err := images.Get(tx, ref)
|
image, err := imageStore.Get(ctx, ref)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "could not resolve %q", ref)
|
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.
|
// 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)
|
diffIDs, err := image.RootFS(ctx, provider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -14,14 +14,15 @@ import (
|
||||||
|
|
||||||
gocontext "context"
|
gocontext "context"
|
||||||
|
|
||||||
"github.com/boltdb/bolt"
|
|
||||||
contentapi "github.com/containerd/containerd/api/services/content"
|
contentapi "github.com/containerd/containerd/api/services/content"
|
||||||
"github.com/containerd/containerd/api/services/execution"
|
"github.com/containerd/containerd/api/services/execution"
|
||||||
|
imagesapi "github.com/containerd/containerd/api/services/images"
|
||||||
rootfsapi "github.com/containerd/containerd/api/services/rootfs"
|
rootfsapi "github.com/containerd/containerd/api/services/rootfs"
|
||||||
"github.com/containerd/containerd/api/types/container"
|
"github.com/containerd/containerd/api/types/container"
|
||||||
"github.com/containerd/containerd/content"
|
"github.com/containerd/containerd/content"
|
||||||
"github.com/containerd/containerd/images"
|
"github.com/containerd/containerd/images"
|
||||||
contentservice "github.com/containerd/containerd/services/content"
|
contentservice "github.com/containerd/containerd/services/content"
|
||||||
|
imagesservice "github.com/containerd/containerd/services/images"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/tonistiigi/fifo"
|
"github.com/tonistiigi/fifo"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
|
@ -134,25 +135,12 @@ func getRootFSService(context *cli.Context) (rootfsapi.RootFSClient, error) {
|
||||||
return rootfsapi.NewRootFSClient(conn), nil
|
return rootfsapi.NewRootFSClient(conn), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDB(ctx *cli.Context, readonly bool) (*bolt.DB, error) {
|
func getImageStore(clicontext *cli.Context) (images.Store, error) {
|
||||||
// TODO(stevvooe): For now, we operate directly on the database. We will
|
conn, err := getGRPCConnection(clicontext)
|
||||||
// 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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return imagesservice.NewStoreFromClient(imagesapi.NewImagesClient(conn)), nil
|
||||||
if !readonly {
|
|
||||||
if err := images.InitDB(db); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return db, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTempDir(id string) (string, error) {
|
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"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/boltdb/bolt"
|
imagesapi "github.com/containerd/containerd/api/services/images"
|
||||||
"github.com/containerd/containerd/content"
|
"github.com/containerd/containerd/content"
|
||||||
"github.com/containerd/containerd/images"
|
"github.com/containerd/containerd/images"
|
||||||
"github.com/containerd/containerd/remotes"
|
"github.com/containerd/containerd/remotes"
|
||||||
"github.com/containerd/containerd/remotes/docker"
|
"github.com/containerd/containerd/remotes/docker"
|
||||||
|
imagesservice "github.com/containerd/containerd/services/images"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
)
|
)
|
||||||
|
@ -27,6 +28,14 @@ func resolveContentStore(context *cli.Context) (*content.Store, error) {
|
||||||
return content.NewStore(root)
|
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) {
|
func connectGRPC(context *cli.Context) (*grpc.ClientConn, error) {
|
||||||
socket := context.GlobalString("socket")
|
socket := context.GlobalString("socket")
|
||||||
timeout := context.GlobalDuration("connect-timeout")
|
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.
|
// getResolver prepares the resolver from the environment and options.
|
||||||
func getResolver(ctx context.Context) (remotes.Resolver, error) {
|
func getResolver(ctx context.Context) (remotes.Resolver, error) {
|
||||||
return docker.NewResolver(), nil
|
return docker.NewResolver(), nil
|
||||||
|
|
27
cmd/dist/images.go
vendored
27
cmd/dist/images.go
vendored
|
@ -6,7 +6,6 @@ import (
|
||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
|
|
||||||
contentapi "github.com/containerd/containerd/api/services/content"
|
contentapi "github.com/containerd/containerd/api/services/content"
|
||||||
"github.com/containerd/containerd/images"
|
|
||||||
"github.com/containerd/containerd/log"
|
"github.com/containerd/containerd/log"
|
||||||
"github.com/containerd/containerd/progress"
|
"github.com/containerd/containerd/progress"
|
||||||
contentservice "github.com/containerd/containerd/services/content"
|
contentservice "github.com/containerd/containerd/services/content"
|
||||||
|
@ -25,23 +24,19 @@ var imagesCommand = cli.Command{
|
||||||
ctx = background
|
ctx = background
|
||||||
)
|
)
|
||||||
|
|
||||||
db, err := getDB(clicontext, true)
|
imageStore, err := resolveImageStore(clicontext)
|
||||||
if err != nil {
|
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)
|
conn, err := connectGRPC(clicontext)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
provider := contentservice.NewProviderFromClient(contentapi.NewContentClient(conn))
|
provider := contentservice.NewProviderFromClient(contentapi.NewContentClient(conn))
|
||||||
|
|
||||||
images, err := images.List(tx)
|
images, err := imageStore.List(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to list images")
|
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)
|
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()
|
tw.Flush()
|
||||||
|
|
||||||
|
@ -74,19 +69,13 @@ var rmiCommand = cli.Command{
|
||||||
exitErr error
|
exitErr error
|
||||||
)
|
)
|
||||||
|
|
||||||
db, err := getDB(clicontext, false)
|
imageStore, err := resolveImageStore(clicontext)
|
||||||
if err != nil {
|
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() {
|
for _, target := range clicontext.Args() {
|
||||||
if err := images.Delete(tx, target); err != nil {
|
if err := imageStore.Delete(ctx, target); err != nil {
|
||||||
if exitErr == nil {
|
if exitErr == nil {
|
||||||
exitErr = errors.Wrapf(err, "unable to delete %v", target)
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := getDB(clicontext, false)
|
imageStore, err := resolveImageStore(clicontext)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
tx, err := db.Begin(true)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer tx.Rollback()
|
|
||||||
|
|
||||||
resolver, err := getResolver(ctx)
|
resolver, err := getResolver(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -65,6 +58,7 @@ command. As part of this process, we do the following:
|
||||||
}
|
}
|
||||||
ongoing := newJobs()
|
ongoing := newJobs()
|
||||||
|
|
||||||
|
// TODO(stevvooe): Must unify this type.
|
||||||
ingester := contentservice.NewIngesterFromClient(contentapi.NewContentClient(conn))
|
ingester := contentservice.NewIngesterFromClient(contentapi.NewContentClient(conn))
|
||||||
provider := contentservice.NewProviderFromClient(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)
|
close(resolved)
|
||||||
|
|
||||||
eg.Go(func() error {
|
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,
|
return images.Dispatch(ctx,
|
||||||
images.Handlers(images.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
|
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() {
|
defer func() {
|
||||||
ctx := context.Background()
|
ctx := background
|
||||||
tx, err := db.Begin(false)
|
|
||||||
if err != nil {
|
|
||||||
log.G(ctx).Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(stevvooe): This section unpacks the layers and resolves the
|
// TODO(stevvooe): This section unpacks the layers and resolves the
|
||||||
// root filesystem chainid for the image. For now, we just print
|
// root filesystem chainid for the image. For now, we just print
|
||||||
// it, but we should keep track of this in the metadata storage.
|
// 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 {
|
if err != nil {
|
||||||
log.G(ctx).Fatal(err)
|
log.G(ctx).Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
provider := contentservice.NewProviderFromClient(contentapi.NewContentClient(conn))
|
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 {
|
if err != nil {
|
||||||
log.G(ctx).Fatal(err)
|
log.G(ctx).Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ import (
|
||||||
// Image provides the model for how containerd views container images.
|
// Image provides the model for how containerd views container images.
|
||||||
type Image struct {
|
type Image struct {
|
||||||
Name string
|
Name string
|
||||||
Descriptor ocispec.Descriptor
|
Target ocispec.Descriptor
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(stevvooe): Many of these functions make strong platform assumptions,
|
// 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) {
|
func (image *Image) Config(ctx context.Context, provider content.Provider) (ocispec.Descriptor, error) {
|
||||||
var configDesc ocispec.Descriptor
|
var configDesc ocispec.Descriptor
|
||||||
return configDesc, Walk(ctx, HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
|
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:
|
case MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest:
|
||||||
rc, err := provider.Reader(ctx, image.Descriptor.Digest)
|
rc, err := provider.Reader(ctx, image.Target.Digest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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")
|
return nil, errors.New("could not resolve config")
|
||||||
}
|
}
|
||||||
|
|
||||||
}), image.Descriptor)
|
}), image.Target)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RootFS returns the unpacked diffids that make up and images rootfs.
|
// 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) {
|
func (image *Image) Size(ctx context.Context, provider content.Provider) (int64, error) {
|
||||||
var size int64
|
var size int64
|
||||||
return size, Walk(ctx, HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
|
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:
|
case MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest:
|
||||||
size += desc.Size
|
size += desc.Size
|
||||||
rc, err := provider.Reader(ctx, image.Descriptor.Digest)
|
rc, err := provider.Reader(ctx, image.Target.Digest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -121,5 +121,5 @@ func (image *Image) Size(ctx context.Context, provider content.Provider) (int64,
|
||||||
return nil, errors.New("unsupported type")
|
return nil, errors.New("unsupported type")
|
||||||
}
|
}
|
||||||
|
|
||||||
}), image.Descriptor)
|
}), image.Target)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package images
|
package images
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
@ -8,12 +9,30 @@ import (
|
||||||
"github.com/containerd/containerd/log"
|
"github.com/containerd/containerd/log"
|
||||||
digest "github.com/opencontainers/go-digest"
|
digest "github.com/opencontainers/go-digest"
|
||||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
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 (
|
var (
|
||||||
bucketKeyStorageVersion = []byte("v1")
|
bucketKeyStorageVersion = []byte("v1")
|
||||||
bucketKeyImages = []byte("images")
|
bucketKeyImages = []byte("images")
|
||||||
|
@ -26,6 +45,8 @@ var (
|
||||||
// "metadata" store. For now, it is bound tightly to the local machine and bolt
|
// "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.
|
// 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 {
|
func InitDB(db *bolt.DB) error {
|
||||||
log.L.Debug("init db")
|
log.L.Debug("init db")
|
||||||
return db.Update(func(tx *bolt.Tx) error {
|
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 {
|
func NewImageStore(tx *bolt.Tx) Store {
|
||||||
return withImagesBucket(tx, func(bkt *bolt.Bucket) error {
|
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))
|
ibkt, err := bkt.CreateBucketIfNotExists([]byte(name))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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) {
|
func (s *storage) List(ctx context.Context) ([]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) {
|
|
||||||
var images []Image
|
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 {
|
return bkt.ForEach(func(k, v []byte) error {
|
||||||
var (
|
var (
|
||||||
image = Image{
|
image = Image{
|
||||||
|
@ -103,8 +132,8 @@ func List(tx *bolt.Tx) ([]Image, error) {
|
||||||
return images, nil
|
return images, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Delete(tx *bolt.Tx, name string) error {
|
func (s *storage) Delete(ctx context.Context, name string) error {
|
||||||
return withImagesBucket(tx, func(bkt *bolt.Bucket) error {
|
return withImagesBucket(s.tx, func(bkt *bolt.Bucket) error {
|
||||||
return bkt.DeleteBucket([]byte(name))
|
return bkt.DeleteBucket([]byte(name))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -119,11 +148,11 @@ func readImage(image *Image, bkt *bolt.Bucket) error {
|
||||||
// keys, rather than full arrays.
|
// keys, rather than full arrays.
|
||||||
switch string(k) {
|
switch string(k) {
|
||||||
case string(bucketKeyDigest):
|
case string(bucketKeyDigest):
|
||||||
image.Descriptor.Digest = digest.Digest(v)
|
image.Target.Digest = digest.Digest(v)
|
||||||
case string(bucketKeyMediaType):
|
case string(bucketKeyMediaType):
|
||||||
image.Descriptor.MediaType = string(v)
|
image.Target.MediaType = string(v)
|
||||||
case string(bucketKeySize):
|
case string(bucketKeySize):
|
||||||
image.Descriptor.Size, _ = binary.Varint(v)
|
image.Target.Size, _ = binary.Varint(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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 {
|
func withImagesBucket(tx *bolt.Tx, fn func(bkt *bolt.Bucket) error) error {
|
||||||
bkt := getImagesBucket(tx)
|
bkt := getImagesBucket(tx)
|
||||||
if bkt == nil {
|
if bkt == nil {
|
||||||
return errImageUnknown
|
return ErrNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
return fn(bkt)
|
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 {
|
func withImageBucket(tx *bolt.Tx, name string, fn func(bkt *bolt.Bucket) error) error {
|
||||||
bkt := getImageBucket(tx, name)
|
bkt := getImageBucket(tx, name)
|
||||||
if bkt == nil {
|
if bkt == nil {
|
||||||
return errImageUnknown
|
return ErrNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
return fn(bkt)
|
return fn(bkt)
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/boltdb/bolt"
|
||||||
"github.com/containerd/containerd"
|
"github.com/containerd/containerd"
|
||||||
"github.com/containerd/containerd/content"
|
"github.com/containerd/containerd/content"
|
||||||
"github.com/containerd/containerd/snapshot"
|
"github.com/containerd/containerd/snapshot"
|
||||||
|
@ -32,7 +33,8 @@ type InitContext struct {
|
||||||
Root string
|
Root string
|
||||||
State string
|
State string
|
||||||
Runtimes map[string]containerd.Runtime
|
Runtimes map[string]containerd.Runtime
|
||||||
Store *content.Store
|
Content *content.Store
|
||||||
|
Meta *bolt.DB
|
||||||
Snapshotter snapshot.Snapshotter
|
Snapshotter snapshot.Snapshotter
|
||||||
Config interface{}
|
Config interface{}
|
||||||
Context context.Context
|
Context context.Context
|
||||||
|
|
|
@ -38,7 +38,7 @@ func init() {
|
||||||
|
|
||||||
func NewService(ic *plugin.InitContext) (interface{}, error) {
|
func NewService(ic *plugin.InitContext) (interface{}, error) {
|
||||||
return &Service{
|
return &Service{
|
||||||
store: ic.Store,
|
store: ic.Content,
|
||||||
}, nil
|
}, 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{
|
plugin.Register("rootfs-grpc", &plugin.Registration{
|
||||||
Type: plugin.GRPCPlugin,
|
Type: plugin.GRPCPlugin,
|
||||||
Init: func(ic *plugin.InitContext) (interface{}, error) {
|
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