From a18f77bea0a7193b6594af3ca22cd2734d6dfb14 Mon Sep 17 00:00:00 2001 From: Kathryn Baldauf Date: Thu, 19 Dec 2019 15:00:37 -0800 Subject: [PATCH] create local version of introspection service Signed-off-by: Kathryn Baldauf --- client.go | 8 +- cmd/ctr/commands/plugins/plugins.go | 5 +- install.go | 8 +- services.go | 28 ++- services/introspection/introspection.go | 62 +++++++ services/introspection/local.go | 227 ++++++++++++++++++++++++ services/introspection/service.go | 205 ++++----------------- services/services.go | 2 + 8 files changed, 354 insertions(+), 191 deletions(-) create mode 100644 services/introspection/introspection.go create mode 100644 services/introspection/local.go diff --git a/client.go b/client.go index 99141e2db..f65b03827 100644 --- a/client.go +++ b/client.go @@ -54,6 +54,7 @@ import ( "github.com/containerd/containerd/plugin" "github.com/containerd/containerd/remotes" "github.com/containerd/containerd/remotes/docker" + "github.com/containerd/containerd/services/introspection" "github.com/containerd/containerd/snapshots" snproxy "github.com/containerd/containerd/snapshots/proxy" "github.com/containerd/typeurl" @@ -621,10 +622,13 @@ func (c *Client) DiffService() DiffService { } // IntrospectionService returns the underlying Introspection Client -func (c *Client) IntrospectionService() introspectionapi.IntrospectionClient { +func (c *Client) IntrospectionService() introspection.Service { + if c.introspectionService != nil { + return c.introspectionService + } c.connMu.Lock() defer c.connMu.Unlock() - return introspectionapi.NewIntrospectionClient(c.conn) + return introspection.NewIntrospectionServiceFromClient(introspectionapi.NewIntrospectionClient(c.conn)) } // LeasesService returns the underlying Leases Client diff --git a/cmd/ctr/commands/plugins/plugins.go b/cmd/ctr/commands/plugins/plugins.go index a7f42192c..c4c449079 100644 --- a/cmd/ctr/commands/plugins/plugins.go +++ b/cmd/ctr/commands/plugins/plugins.go @@ -23,7 +23,6 @@ import ( "strings" "text/tabwriter" - introspection "github.com/containerd/containerd/api/services/introspection/v1" "github.com/containerd/containerd/api/types" "github.com/containerd/containerd/cmd/ctr/commands" "github.com/containerd/containerd/platforms" @@ -67,9 +66,7 @@ var listCommand = cli.Command{ } defer cancel() ps := client.IntrospectionService() - response, err := ps.Plugins(ctx, &introspection.PluginsRequest{ - Filters: context.Args(), - }) + response, err := ps.Plugins(ctx, context.Args()) if err != nil { return err } diff --git a/install.go b/install.go index df6c8bc8a..7a8311c83 100644 --- a/install.go +++ b/install.go @@ -24,7 +24,6 @@ import ( "runtime" "strings" - introspectionapi "github.com/containerd/containerd/api/services/introspection/v1" "github.com/containerd/containerd/archive" "github.com/containerd/containerd/archive/compression" "github.com/containerd/containerd/content" @@ -99,11 +98,8 @@ func (c *Client) getInstallPath(ctx context.Context, config InstallConfig) (stri if config.Path != "" { return config.Path, nil } - resp, err := c.IntrospectionService().Plugins(ctx, &introspectionapi.PluginsRequest{ - Filters: []string{ - "id==opt", - }, - }) + filters := []string{"id==opt"} + resp, err := c.IntrospectionService().Plugins(ctx, filters) if err != nil { return "", err } diff --git a/services.go b/services.go index 395fc3065..d8fced559 100644 --- a/services.go +++ b/services.go @@ -20,6 +20,7 @@ import ( containersapi "github.com/containerd/containerd/api/services/containers/v1" "github.com/containerd/containerd/api/services/diff/v1" imagesapi "github.com/containerd/containerd/api/services/images/v1" + introspectionapi "github.com/containerd/containerd/api/services/introspection/v1" namespacesapi "github.com/containerd/containerd/api/services/namespaces/v1" "github.com/containerd/containerd/api/services/tasks/v1" "github.com/containerd/containerd/containers" @@ -27,19 +28,21 @@ import ( "github.com/containerd/containerd/images" "github.com/containerd/containerd/leases" "github.com/containerd/containerd/namespaces" + "github.com/containerd/containerd/services/introspection" "github.com/containerd/containerd/snapshots" ) type services struct { - contentStore content.Store - imageStore images.Store - containerStore containers.Store - namespaceStore namespaces.Store - snapshotters map[string]snapshots.Snapshotter - taskService tasks.TasksClient - diffService DiffService - eventService EventService - leasesService leases.Manager + contentStore content.Store + imageStore images.Store + containerStore containers.Store + namespaceStore namespaces.Store + snapshotters map[string]snapshots.Snapshotter + taskService tasks.TasksClient + diffService DiffService + eventService EventService + leasesService leases.Manager + introspectionService introspection.Service } // ServicesOpt allows callers to set options on the services @@ -110,3 +113,10 @@ func WithLeasesService(leasesService leases.Manager) ServicesOpt { s.leasesService = leasesService } } + +// WithIntrospectionService sets the introspection service. +func WithIntrospectionService(in introspectionapi.IntrospectionClient) ServicesOpt { + return func(s *services) { + s.introspectionService = introspection.NewIntrospectionServiceFromClient(in) + } +} diff --git a/services/introspection/introspection.go b/services/introspection/introspection.go new file mode 100644 index 000000000..5024788b5 --- /dev/null +++ b/services/introspection/introspection.go @@ -0,0 +1,62 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package introspection + +import ( + context "context" + + api "github.com/containerd/containerd/api/services/introspection/v1" + "github.com/containerd/containerd/errdefs" + ptypes "github.com/gogo/protobuf/types" +) + +type Service interface { + Plugins(context.Context, []string) (*api.PluginsResponse, error) + Server(context.Context, *ptypes.Empty) (*api.ServerResponse, error) +} + +type introspectionRemote struct { + client api.IntrospectionClient +} + +var _ = (Service)(&introspectionRemote{}) + +func NewIntrospectionServiceFromClient(c api.IntrospectionClient) Service { + return &introspectionRemote{client: c} +} + +func (i *introspectionRemote) Plugins(ctx context.Context, filters []string) (*api.PluginsResponse, error) { + resp, err := i.client.Plugins(ctx, &api.PluginsRequest{ + Filters: filters, + }) + + if err != nil { + return nil, errdefs.FromGRPC(err) + } + + return resp, nil +} + +func (i *introspectionRemote) Server(ctx context.Context, in *ptypes.Empty) (*api.ServerResponse, error) { + resp, err := i.client.Server(ctx, in) + + if err != nil { + return nil, errdefs.FromGRPC(err) + } + + return resp, nil +} diff --git a/services/introspection/local.go b/services/introspection/local.go new file mode 100644 index 000000000..9b6875a00 --- /dev/null +++ b/services/introspection/local.go @@ -0,0 +1,227 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package introspection + +import ( + context "context" + "io/ioutil" + "os" + "path/filepath" + "sync" + + api "github.com/containerd/containerd/api/services/introspection/v1" + "github.com/containerd/containerd/api/types" + "github.com/containerd/containerd/errdefs" + "github.com/containerd/containerd/filters" + "github.com/containerd/containerd/plugin" + "github.com/containerd/containerd/services" + "github.com/gogo/googleapis/google/rpc" + ptypes "github.com/gogo/protobuf/types" + "github.com/google/uuid" + "google.golang.org/grpc" + "google.golang.org/grpc/status" +) + +func init() { + plugin.Register(&plugin.Registration{ + Type: plugin.ServicePlugin, + ID: services.IntrospectionService, + Requires: []plugin.Type{}, + InitFn: func(ic *plugin.InitContext) (interface{}, error) { + // this service works by using the plugin context up till the point + // this service is initialized. Since we require this service last, + // it should provide the full set of plugins. + pluginsPB := pluginsToPB(ic.GetAll()) + return &Local{ + plugins: pluginsPB, + root: ic.Root, + }, nil + }, + }) +} + +type Local struct { + mu sync.Mutex + plugins []api.Plugin + root string +} + +var _ = (api.IntrospectionClient)(&Local{}) + +func (l *Local) UpdateLocal(root string, plugins []api.Plugin) { + l.mu.Lock() + defer l.mu.Unlock() + l.root = root + l.plugins = plugins +} + +func (l *Local) Plugins(ctx context.Context, req *api.PluginsRequest, _ ...grpc.CallOption) (*api.PluginsResponse, error) { + filter, err := filters.ParseAll(req.Filters...) + if err != nil { + return nil, errdefs.ToGRPCf(errdefs.ErrInvalidArgument, err.Error()) + } + + var plugins []api.Plugin + allPlugins := l.getPlugins() + for _, p := range allPlugins { + if !filter.Match(adaptPlugin(p)) { + continue + } + + plugins = append(plugins, p) + } + + return &api.PluginsResponse{ + Plugins: plugins, + }, nil +} + +func (l *Local) getPlugins() []api.Plugin { + l.mu.Lock() + defer l.mu.Unlock() + return l.plugins +} + +func (l *Local) Server(ctx context.Context, _ *ptypes.Empty, _ ...grpc.CallOption) (*api.ServerResponse, error) { + u, err := l.getUUID() + if err != nil { + return nil, errdefs.ToGRPC(err) + } + return &api.ServerResponse{ + UUID: u, + }, nil +} + +func (l *Local) getUUID() (string, error) { + l.mu.Lock() + defer l.mu.Unlock() + + data, err := ioutil.ReadFile(l.uuidPath()) + if err != nil { + if os.IsNotExist(err) { + return l.generateUUID() + } + return "", err + } + u := string(data) + if _, err := uuid.Parse(u); err != nil { + return "", err + } + return u, nil +} + +func (l *Local) generateUUID() (string, error) { + u, err := uuid.NewRandom() + if err != nil { + return "", err + } + path := l.uuidPath() + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { + return "", err + } + uu := u.String() + if err := ioutil.WriteFile(path, []byte(uu), 0666); err != nil { + return "", err + } + return uu, nil +} + +func (l *Local) uuidPath() string { + return filepath.Join(l.root, "uuid") +} + +func adaptPlugin(o interface{}) filters.Adaptor { + obj := o.(api.Plugin) + return filters.AdapterFunc(func(fieldpath []string) (string, bool) { + if len(fieldpath) == 0 { + return "", false + } + + switch fieldpath[0] { + case "type": + return obj.Type, len(obj.Type) > 0 + case "id": + return obj.ID, len(obj.ID) > 0 + case "platforms": + // TODO(stevvooe): Another case here where have multiple values. + // May need to refactor the filter system to allow filtering by + // platform, if this is required. + case "capabilities": + // TODO(stevvooe): Need a better way to match against + // collections. We can only return "the value" but really it + // would be best if we could return a set of values for the + // path, any of which could match. + } + + return "", false + }) +} + +func pluginsToPB(plugins []*plugin.Plugin) []api.Plugin { + var pluginsPB []api.Plugin + for _, p := range plugins { + var platforms []types.Platform + for _, p := range p.Meta.Platforms { + platforms = append(platforms, types.Platform{ + OS: p.OS, + Architecture: p.Architecture, + Variant: p.Variant, + }) + } + + var requires []string + for _, r := range p.Registration.Requires { + requires = append(requires, r.String()) + } + + var initErr *rpc.Status + if err := p.Err(); err != nil { + st, ok := status.FromError(errdefs.ToGRPC(err)) + if ok { + var details []*ptypes.Any + for _, d := range st.Proto().Details { + details = append(details, &ptypes.Any{ + TypeUrl: d.TypeUrl, + Value: d.Value, + }) + } + initErr = &rpc.Status{ + Code: int32(st.Code()), + Message: st.Message(), + Details: details, + } + } else { + initErr = &rpc.Status{ + Code: int32(rpc.UNKNOWN), + Message: err.Error(), + } + } + } + + pluginsPB = append(pluginsPB, api.Plugin{ + Type: p.Registration.Type.String(), + ID: p.Registration.ID, + Requires: requires, + Platforms: platforms, + Capabilities: p.Meta.Capabilities, + Exports: p.Meta.Exports, + InitErr: initErr, + }) + } + + return pluginsPB +} diff --git a/services/introspection/service.go b/services/introspection/service.go index 404ffe481..b8c54afc0 100644 --- a/services/introspection/service.go +++ b/services/introspection/service.go @@ -18,21 +18,13 @@ package introspection import ( context "context" - "io/ioutil" - "os" - "path/filepath" - "sync" api "github.com/containerd/containerd/api/services/introspection/v1" - "github.com/containerd/containerd/api/types" - "github.com/containerd/containerd/errdefs" - "github.com/containerd/containerd/filters" "github.com/containerd/containerd/plugin" - "github.com/gogo/googleapis/google/rpc" + "github.com/containerd/containerd/services" ptypes "github.com/gogo/protobuf/types" - "github.com/google/uuid" + "github.com/pkg/errors" "google.golang.org/grpc" - "google.golang.org/grpc/status" ) func init() { @@ -44,177 +36,50 @@ func init() { // this service works by using the plugin context up till the point // this service is initialized. Since we require this service last, // it should provide the full set of plugins. - pluginsPB := pluginsToPB(ic.GetAll()) - return NewService(pluginsPB, ic.Root), nil + plugins, err := ic.GetByType(plugin.ServicePlugin) + if err != nil { + return nil, err + } + p, ok := plugins[services.IntrospectionService] + if !ok { + return nil, errors.New("introspection service not found") + } + + i, err := p.Instance() + if err != nil { + return nil, err + } + + allPluginsPB := pluginsToPB(ic.GetAll()) + + localClient, ok := i.(*Local) + if !ok { + return nil, errors.Errorf("Could not create a local client for introspection service") + } + localClient.UpdateLocal(ic.Root, allPluginsPB) + + return &server{ + local: localClient, + }, nil }, }) } -type service struct { - mu sync.Mutex - plugins []api.Plugin - root string +type server struct { + local api.IntrospectionClient } -// NewService returns the GRPC introspection server -func NewService(plugins []api.Plugin, root string) api.IntrospectionServer { - return &service{ - plugins: plugins, - root: root, - } -} +var _ = (api.IntrospectionServer)(&server{}) -func (s *service) Register(server *grpc.Server) error { +func (s *server) Register(server *grpc.Server) error { api.RegisterIntrospectionServer(server, s) return nil } -func (s *service) Plugins(ctx context.Context, req *api.PluginsRequest) (*api.PluginsResponse, error) { - filter, err := filters.ParseAll(req.Filters...) - if err != nil { - return nil, errdefs.ToGRPCf(errdefs.ErrInvalidArgument, err.Error()) - } - - var plugins []api.Plugin - for _, p := range s.plugins { - if !filter.Match(adaptPlugin(p)) { - continue - } - - plugins = append(plugins, p) - } - - return &api.PluginsResponse{ - Plugins: plugins, - }, nil +func (s *server) Plugins(ctx context.Context, req *api.PluginsRequest) (*api.PluginsResponse, error) { + return s.local.Plugins(ctx, req) } -func (s *service) Server(ctx context.Context, _ *ptypes.Empty) (*api.ServerResponse, error) { - u, err := s.getUUID() - if err != nil { - return nil, errdefs.ToGRPC(err) - } - return &api.ServerResponse{ - UUID: u, - }, nil -} - -func (s *service) getUUID() (string, error) { - s.mu.Lock() - defer s.mu.Unlock() - - data, err := ioutil.ReadFile(s.uuidPath()) - if err != nil { - if os.IsNotExist(err) { - return s.generateUUID() - } - return "", err - } - u := string(data) - if _, err := uuid.Parse(u); err != nil { - return "", err - } - return u, nil -} - -func (s *service) generateUUID() (string, error) { - u, err := uuid.NewRandom() - if err != nil { - return "", err - } - path := s.uuidPath() - if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { - return "", err - } - uu := u.String() - if err := ioutil.WriteFile(path, []byte(uu), 0666); err != nil { - return "", err - } - return uu, nil -} - -func (s *service) uuidPath() string { - return filepath.Join(s.root, "uuid") -} - -func adaptPlugin(o interface{}) filters.Adaptor { - obj := o.(api.Plugin) - return filters.AdapterFunc(func(fieldpath []string) (string, bool) { - if len(fieldpath) == 0 { - return "", false - } - - switch fieldpath[0] { - case "type": - return obj.Type, len(obj.Type) > 0 - case "id": - return obj.ID, len(obj.ID) > 0 - case "platforms": - // TODO(stevvooe): Another case here where have multiple values. - // May need to refactor the filter system to allow filtering by - // platform, if this is required. - case "capabilities": - // TODO(stevvooe): Need a better way to match against - // collections. We can only return "the value" but really it - // would be best if we could return a set of values for the - // path, any of which could match. - } - - return "", false - }) -} - -func pluginsToPB(plugins []*plugin.Plugin) []api.Plugin { - var pluginsPB []api.Plugin - for _, p := range plugins { - var platforms []types.Platform - for _, p := range p.Meta.Platforms { - platforms = append(platforms, types.Platform{ - OS: p.OS, - Architecture: p.Architecture, - Variant: p.Variant, - }) - } - - var requires []string - for _, r := range p.Registration.Requires { - requires = append(requires, r.String()) - } - - var initErr *rpc.Status - if err := p.Err(); err != nil { - st, ok := status.FromError(errdefs.ToGRPC(err)) - if ok { - var details []*ptypes.Any - for _, d := range st.Proto().Details { - details = append(details, &ptypes.Any{ - TypeUrl: d.TypeUrl, - Value: d.Value, - }) - } - initErr = &rpc.Status{ - Code: int32(st.Code()), - Message: st.Message(), - Details: details, - } - } else { - initErr = &rpc.Status{ - Code: int32(rpc.UNKNOWN), - Message: err.Error(), - } - } - } - - pluginsPB = append(pluginsPB, api.Plugin{ - Type: p.Registration.Type.String(), - ID: p.Registration.ID, - Requires: requires, - Platforms: platforms, - Capabilities: p.Meta.Capabilities, - Exports: p.Meta.Exports, - InitErr: initErr, - }) - } - - return pluginsPB +func (s *server) Server(ctx context.Context, empty *ptypes.Empty) (*api.ServerResponse, error) { + return s.local.Server(ctx, empty) } diff --git a/services/services.go b/services/services.go index efc920093..27f47a5ce 100644 --- a/services/services.go +++ b/services/services.go @@ -33,4 +33,6 @@ const ( LeasesService = "leases-service" // DiffService is id of diff service. DiffService = "diff-service" + // IntrospectionService is the id of introspection service + IntrospectionService = "introspection-service" )