
Split service proxy from service plugin. Make introspection service easier for clients to use. Update service proxy to support grpc and ttrpc. Signed-off-by: Derek McGowan <derek@mcg.dev>
320 lines
8.2 KiB
Go
320 lines
8.2 KiB
Go
/*
|
|
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"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sync"
|
|
|
|
"github.com/google/uuid"
|
|
"google.golang.org/genproto/googleapis/rpc/code"
|
|
rpc "google.golang.org/genproto/googleapis/rpc/status"
|
|
"google.golang.org/grpc/status"
|
|
|
|
api "github.com/containerd/containerd/v2/api/services/introspection/v1"
|
|
"github.com/containerd/containerd/v2/api/types"
|
|
"github.com/containerd/containerd/v2/core/introspection"
|
|
"github.com/containerd/containerd/v2/pkg/filters"
|
|
"github.com/containerd/containerd/v2/plugins"
|
|
"github.com/containerd/containerd/v2/plugins/services"
|
|
"github.com/containerd/containerd/v2/plugins/services/warning"
|
|
"github.com/containerd/containerd/v2/protobuf"
|
|
ptypes "github.com/containerd/containerd/v2/protobuf/types"
|
|
"github.com/containerd/errdefs"
|
|
"github.com/containerd/plugin"
|
|
"github.com/containerd/plugin/registry"
|
|
"github.com/containerd/typeurl/v2"
|
|
)
|
|
|
|
func init() {
|
|
registry.Register(&plugin.Registration{
|
|
Type: plugins.ServicePlugin,
|
|
ID: services.IntrospectionService,
|
|
Requires: []plugin.Type{plugins.WarningPlugin},
|
|
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
|
|
i, err := ic.GetByID(plugins.WarningPlugin, plugins.DeprecationsPlugin)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
warningClient, ok := i.(warning.Service)
|
|
if !ok {
|
|
return nil, errors.New("could not create a local client for warning service")
|
|
}
|
|
|
|
// this service fetches all plugins through the plugin set of the plugin context
|
|
return &Local{
|
|
plugins: ic.Plugins(),
|
|
root: ic.Properties[plugins.PropertyRootDir],
|
|
warningClient: warningClient,
|
|
}, nil
|
|
},
|
|
})
|
|
}
|
|
|
|
// Local is a local implementation of the introspection service
|
|
type Local struct {
|
|
mu sync.Mutex
|
|
root string
|
|
plugins *plugin.Set
|
|
pluginCache []*api.Plugin
|
|
warningClient warning.Service
|
|
}
|
|
|
|
var _ = (introspection.Service)(&Local{})
|
|
|
|
// UpdateLocal updates the local introspection service
|
|
func (l *Local) UpdateLocal(root string) {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
l.root = root
|
|
}
|
|
|
|
// Plugins returns the locally defined plugins
|
|
func (l *Local) Plugins(ctx context.Context, fs ...string) (*api.PluginsResponse, error) {
|
|
filter, err := filters.ParseAll(fs...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: %w", errdefs.ErrInvalidArgument, err)
|
|
}
|
|
|
|
var plugins []*api.Plugin
|
|
allPlugins := l.getPlugins()
|
|
for _, p := range allPlugins {
|
|
p := p
|
|
if filter.Match(adaptPlugin(p)) {
|
|
plugins = append(plugins, p)
|
|
}
|
|
}
|
|
|
|
return &api.PluginsResponse{
|
|
Plugins: plugins,
|
|
}, nil
|
|
}
|
|
|
|
func (l *Local) getPlugins() []*api.Plugin {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
plugins := l.plugins.GetAll()
|
|
if l.pluginCache == nil || len(plugins) != len(l.pluginCache) {
|
|
l.pluginCache = pluginsToPB(plugins)
|
|
}
|
|
return l.pluginCache
|
|
}
|
|
|
|
// Server returns the local server information
|
|
func (l *Local) Server(ctx context.Context) (*api.ServerResponse, error) {
|
|
u, err := l.getUUID()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pid := os.Getpid()
|
|
var pidns uint64
|
|
if runtime.GOOS == "linux" {
|
|
pidns, err = statPIDNS(pid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return &api.ServerResponse{
|
|
UUID: u,
|
|
Pid: uint64(pid),
|
|
Pidns: pidns,
|
|
Deprecations: l.getWarnings(ctx),
|
|
}, nil
|
|
}
|
|
|
|
func (l *Local) getUUID() (string, error) {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
|
|
data, err := os.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 := os.WriteFile(path, []byte(uu), 0666); err != nil {
|
|
return "", err
|
|
}
|
|
return uu, nil
|
|
}
|
|
|
|
func (l *Local) uuidPath() string {
|
|
return filepath.Join(l.root, "uuid")
|
|
}
|
|
|
|
func (l *Local) getWarnings(ctx context.Context) []*api.DeprecationWarning {
|
|
return warningsPB(ctx, l.warningClient.Warnings())
|
|
}
|
|
|
|
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 pluginToPB(p *plugin.Plugin) *api.Plugin {
|
|
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(code.Code_UNKNOWN),
|
|
Message: err.Error(),
|
|
}
|
|
}
|
|
}
|
|
|
|
return &api.Plugin{
|
|
Type: p.Registration.Type.String(),
|
|
ID: p.Registration.ID,
|
|
Requires: requires,
|
|
Platforms: types.OCIPlatformToProto(p.Meta.Platforms),
|
|
Capabilities: p.Meta.Capabilities,
|
|
Exports: p.Meta.Exports,
|
|
InitErr: initErr,
|
|
}
|
|
}
|
|
|
|
func pluginsToPB(plugins []*plugin.Plugin) []*api.Plugin {
|
|
pluginsPB := make([]*api.Plugin, 0, len(plugins))
|
|
for _, p := range plugins {
|
|
pluginsPB = append(pluginsPB, pluginToPB(p))
|
|
}
|
|
|
|
return pluginsPB
|
|
}
|
|
|
|
func warningsPB(ctx context.Context, warnings []warning.Warning) []*api.DeprecationWarning {
|
|
var pb []*api.DeprecationWarning
|
|
|
|
for _, w := range warnings {
|
|
pb = append(pb, &api.DeprecationWarning{
|
|
ID: string(w.ID),
|
|
Message: w.Message,
|
|
LastOccurrence: protobuf.ToTimestamp(w.LastOccurrence),
|
|
})
|
|
}
|
|
return pb
|
|
}
|
|
|
|
type pluginInfoProvider interface {
|
|
PluginInfo(context.Context, interface{}) (interface{}, error)
|
|
}
|
|
|
|
func (l *Local) PluginInfo(ctx context.Context, pluginType, id string, options any) (*api.PluginInfoResponse, error) {
|
|
p := l.plugins.Get(plugin.Type(pluginType), id)
|
|
if p == nil {
|
|
return nil, fmt.Errorf("plugin %s.%s not found: %w", pluginType, id, errdefs.ErrNotFound)
|
|
}
|
|
|
|
resp := &api.PluginInfoResponse{
|
|
Plugin: pluginToPB(p),
|
|
}
|
|
|
|
// Request additional info from plugin instance
|
|
if options != nil {
|
|
if p.Err() != nil {
|
|
return resp, fmt.Errorf("cannot get extra info, plugin not successfully loaded: %w", errdefs.ErrFailedPrecondition)
|
|
}
|
|
inst, err := p.Instance()
|
|
if err != nil {
|
|
return resp, fmt.Errorf("failed to get plugin instance: %w", errdefs.ErrFailedPrecondition)
|
|
}
|
|
pi, ok := inst.(pluginInfoProvider)
|
|
if !ok {
|
|
return resp, fmt.Errorf("plugin does not provided extra information: %w", errdefs.ErrNotImplemented)
|
|
}
|
|
|
|
info, err := pi.PluginInfo(ctx, options)
|
|
if err != nil {
|
|
return resp, errdefs.ToGRPC(err)
|
|
}
|
|
ai, err := typeurl.MarshalAny(info)
|
|
if err != nil {
|
|
return resp, fmt.Errorf("failed to marshal plugin info: %w", err)
|
|
}
|
|
resp.Extra = &ptypes.Any{
|
|
TypeUrl: ai.GetTypeUrl(),
|
|
Value: ai.GetValue(),
|
|
}
|
|
}
|
|
return resp, nil
|
|
}
|