github.com/rish1988/moby@v25.0.2+incompatible/plugin/manager.go (about)

     1  package plugin // import "github.com/docker/docker/plugin"
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"io"
     7  	"os"
     8  	"path/filepath"
     9  	"reflect"
    10  	"regexp"
    11  	"sort"
    12  	"strings"
    13  	"sync"
    14  	"syscall"
    15  
    16  	"github.com/containerd/containerd/content"
    17  	"github.com/containerd/containerd/content/local"
    18  	"github.com/containerd/log"
    19  	"github.com/docker/docker/api/types"
    20  	"github.com/docker/docker/api/types/events"
    21  	"github.com/docker/docker/pkg/authorization"
    22  	"github.com/docker/docker/pkg/containerfs"
    23  	"github.com/docker/docker/pkg/ioutils"
    24  	v2 "github.com/docker/docker/plugin/v2"
    25  	"github.com/docker/docker/registry"
    26  	"github.com/moby/pubsub"
    27  	"github.com/opencontainers/go-digest"
    28  	specs "github.com/opencontainers/runtime-spec/specs-go"
    29  	"github.com/pkg/errors"
    30  	"github.com/sirupsen/logrus"
    31  )
    32  
    33  const (
    34  	configFileName = "config.json"
    35  	rootFSFileName = "rootfs"
    36  )
    37  
    38  var validFullID = regexp.MustCompile(`^([a-f0-9]{64})$`)
    39  
    40  // Executor is the interface that the plugin manager uses to interact with for starting/stopping plugins
    41  type Executor interface {
    42  	Create(id string, spec specs.Spec, stdout, stderr io.WriteCloser) error
    43  	IsRunning(id string) (bool, error)
    44  	Restore(id string, stdout, stderr io.WriteCloser) (alive bool, err error)
    45  	Signal(id string, signal syscall.Signal) error
    46  }
    47  
    48  // EndpointResolver provides looking up registry endpoints for pulling.
    49  type EndpointResolver interface {
    50  	LookupPullEndpoints(hostname string) (endpoints []registry.APIEndpoint, err error)
    51  }
    52  
    53  func (pm *Manager) restorePlugin(p *v2.Plugin, c *controller) error {
    54  	if p.IsEnabled() {
    55  		return pm.restore(p, c)
    56  	}
    57  	return nil
    58  }
    59  
    60  type eventLogger func(id, name string, action events.Action)
    61  
    62  // ManagerConfig defines configuration needed to start new manager.
    63  type ManagerConfig struct {
    64  	Store              *Store // remove
    65  	RegistryService    EndpointResolver
    66  	LiveRestoreEnabled bool // TODO: remove
    67  	LogPluginEvent     eventLogger
    68  	Root               string
    69  	ExecRoot           string
    70  	CreateExecutor     ExecutorCreator
    71  	AuthzMiddleware    *authorization.Middleware
    72  }
    73  
    74  // ExecutorCreator is used in the manager config to pass in an `Executor`
    75  type ExecutorCreator func(*Manager) (Executor, error)
    76  
    77  // Manager controls the plugin subsystem.
    78  type Manager struct {
    79  	config    ManagerConfig
    80  	mu        sync.RWMutex // protects cMap
    81  	muGC      sync.RWMutex // protects blobstore deletions
    82  	cMap      map[*v2.Plugin]*controller
    83  	blobStore content.Store
    84  	publisher *pubsub.Publisher
    85  	executor  Executor
    86  }
    87  
    88  // controller represents the manager's control on a plugin.
    89  type controller struct {
    90  	restart       bool
    91  	exitChan      chan bool
    92  	timeoutInSecs int
    93  }
    94  
    95  // NewManager returns a new plugin manager.
    96  func NewManager(config ManagerConfig) (*Manager, error) {
    97  	manager := &Manager{
    98  		config: config,
    99  	}
   100  	for _, dirName := range []string{manager.config.Root, manager.config.ExecRoot, manager.tmpDir()} {
   101  		if err := os.MkdirAll(dirName, 0o700); err != nil {
   102  			return nil, errors.Wrapf(err, "failed to mkdir %v", dirName)
   103  		}
   104  	}
   105  	var err error
   106  	manager.executor, err = config.CreateExecutor(manager)
   107  	if err != nil {
   108  		return nil, err
   109  	}
   110  
   111  	manager.blobStore, err = local.NewStore(filepath.Join(manager.config.Root, "storage"))
   112  	if err != nil {
   113  		return nil, errors.Wrap(err, "error creating plugin blob store")
   114  	}
   115  
   116  	manager.cMap = make(map[*v2.Plugin]*controller)
   117  	if err := manager.reload(); err != nil {
   118  		return nil, errors.Wrap(err, "failed to restore plugins")
   119  	}
   120  
   121  	manager.publisher = pubsub.NewPublisher(0, 0)
   122  	return manager, nil
   123  }
   124  
   125  func (pm *Manager) tmpDir() string {
   126  	return filepath.Join(pm.config.Root, "tmp")
   127  }
   128  
   129  // HandleExitEvent is called when the executor receives the exit event
   130  // In the future we may change this, but for now all we care about is the exit event.
   131  func (pm *Manager) HandleExitEvent(id string) error {
   132  	p, err := pm.config.Store.GetV2Plugin(id)
   133  	if err != nil {
   134  		return err
   135  	}
   136  
   137  	if err := os.RemoveAll(filepath.Join(pm.config.ExecRoot, id)); err != nil {
   138  		log.G(context.TODO()).WithError(err).WithField("id", id).Error("Could not remove plugin bundle dir")
   139  	}
   140  
   141  	pm.mu.RLock()
   142  	c := pm.cMap[p]
   143  	if c.exitChan != nil {
   144  		close(c.exitChan)
   145  		c.exitChan = nil // ignore duplicate events (containerd issue #2299)
   146  	}
   147  	restart := c.restart
   148  	pm.mu.RUnlock()
   149  
   150  	if restart {
   151  		pm.enable(p, c, true)
   152  	} else if err := recursiveUnmount(filepath.Join(pm.config.Root, id)); err != nil {
   153  		return errors.Wrap(err, "error cleaning up plugin mounts")
   154  	}
   155  	return nil
   156  }
   157  
   158  func handleLoadError(err error, id string) {
   159  	if err == nil {
   160  		return
   161  	}
   162  	logger := log.G(context.TODO()).WithError(err).WithField("id", id)
   163  	if errors.Is(err, os.ErrNotExist) {
   164  		// Likely some error while removing on an older version of docker
   165  		logger.Warn("missing plugin config, skipping: this may be caused due to a failed remove and requires manual cleanup.")
   166  		return
   167  	}
   168  	logger.Error("error loading plugin, skipping")
   169  }
   170  
   171  func (pm *Manager) reload() error { // todo: restore
   172  	dir, err := os.ReadDir(pm.config.Root)
   173  	if err != nil {
   174  		return errors.Wrapf(err, "failed to read %v", pm.config.Root)
   175  	}
   176  	plugins := make(map[string]*v2.Plugin)
   177  	for _, v := range dir {
   178  		if validFullID.MatchString(v.Name()) {
   179  			p, err := pm.loadPlugin(v.Name())
   180  			if err != nil {
   181  				handleLoadError(err, v.Name())
   182  				continue
   183  			}
   184  			plugins[p.GetID()] = p
   185  		} else {
   186  			if validFullID.MatchString(strings.TrimSuffix(v.Name(), "-removing")) {
   187  				// There was likely some error while removing this plugin, let's try to remove again here
   188  				if err := containerfs.EnsureRemoveAll(v.Name()); err != nil {
   189  					log.G(context.TODO()).WithError(err).WithField("id", v.Name()).Warn("error while attempting to clean up previously removed plugin")
   190  				}
   191  			}
   192  		}
   193  	}
   194  
   195  	pm.config.Store.SetAll(plugins)
   196  
   197  	var wg sync.WaitGroup
   198  	wg.Add(len(plugins))
   199  	for _, p := range plugins {
   200  		c := &controller{exitChan: make(chan bool)}
   201  		pm.mu.Lock()
   202  		pm.cMap[p] = c
   203  		pm.mu.Unlock()
   204  
   205  		go func(p *v2.Plugin) {
   206  			defer wg.Done()
   207  			if err := pm.restorePlugin(p, c); err != nil {
   208  				log.G(context.TODO()).WithError(err).WithField("id", p.GetID()).Error("Failed to restore plugin")
   209  				return
   210  			}
   211  
   212  			if p.Rootfs != "" {
   213  				p.Rootfs = filepath.Join(pm.config.Root, p.PluginObj.ID, "rootfs")
   214  			}
   215  
   216  			// We should only enable rootfs propagation for certain plugin types that need it.
   217  			for _, typ := range p.PluginObj.Config.Interface.Types {
   218  				if (typ.Capability == "volumedriver" || typ.Capability == "graphdriver" || typ.Capability == "csinode" || typ.Capability == "csicontroller") && typ.Prefix == "docker" && strings.HasPrefix(typ.Version, "1.") {
   219  					if p.PluginObj.Config.PropagatedMount != "" {
   220  						propRoot := filepath.Join(filepath.Dir(p.Rootfs), "propagated-mount")
   221  
   222  						// check if we need to migrate an older propagated mount from before
   223  						// these mounts were stored outside the plugin rootfs
   224  						if _, err := os.Stat(propRoot); os.IsNotExist(err) {
   225  							rootfsProp := filepath.Join(p.Rootfs, p.PluginObj.Config.PropagatedMount)
   226  							if _, err := os.Stat(rootfsProp); err == nil {
   227  								if err := os.Rename(rootfsProp, propRoot); err != nil {
   228  									log.G(context.TODO()).WithError(err).WithField("dir", propRoot).Error("error migrating propagated mount storage")
   229  								}
   230  							}
   231  						}
   232  
   233  						if err := os.MkdirAll(propRoot, 0o755); err != nil {
   234  							log.G(context.TODO()).Errorf("failed to create PropagatedMount directory at %s: %v", propRoot, err)
   235  						}
   236  					}
   237  				}
   238  			}
   239  
   240  			pm.save(p)
   241  			requiresManualRestore := !pm.config.LiveRestoreEnabled && p.IsEnabled()
   242  
   243  			if requiresManualRestore {
   244  				// if liveRestore is not enabled, the plugin will be stopped now so we should enable it
   245  				if err := pm.enable(p, c, true); err != nil {
   246  					log.G(context.TODO()).WithError(err).WithField("id", p.GetID()).Error("failed to enable plugin")
   247  				}
   248  			}
   249  		}(p)
   250  	}
   251  	wg.Wait()
   252  	return nil
   253  }
   254  
   255  // Get looks up the requested plugin in the store.
   256  func (pm *Manager) Get(idOrName string) (*v2.Plugin, error) {
   257  	return pm.config.Store.GetV2Plugin(idOrName)
   258  }
   259  
   260  func (pm *Manager) loadPlugin(id string) (*v2.Plugin, error) {
   261  	p := filepath.Join(pm.config.Root, id, configFileName)
   262  	dt, err := os.ReadFile(p)
   263  	if err != nil {
   264  		return nil, errors.Wrapf(err, "error reading %v", p)
   265  	}
   266  	var plugin v2.Plugin
   267  	if err := json.Unmarshal(dt, &plugin); err != nil {
   268  		return nil, errors.Wrapf(err, "error decoding %v", p)
   269  	}
   270  	return &plugin, nil
   271  }
   272  
   273  func (pm *Manager) save(p *v2.Plugin) error {
   274  	pluginJSON, err := json.Marshal(p)
   275  	if err != nil {
   276  		return errors.Wrap(err, "failed to marshal plugin json")
   277  	}
   278  	if err := ioutils.AtomicWriteFile(filepath.Join(pm.config.Root, p.GetID(), configFileName), pluginJSON, 0o600); err != nil {
   279  		return errors.Wrap(err, "failed to write atomically plugin json")
   280  	}
   281  	return nil
   282  }
   283  
   284  // GC cleans up unreferenced blobs. This is recommended to run in a goroutine
   285  func (pm *Manager) GC() {
   286  	pm.muGC.Lock()
   287  	defer pm.muGC.Unlock()
   288  
   289  	used := make(map[digest.Digest]struct{})
   290  	for _, p := range pm.config.Store.GetAll() {
   291  		used[p.Config] = struct{}{}
   292  		for _, b := range p.Blobsums {
   293  			used[b] = struct{}{}
   294  		}
   295  	}
   296  
   297  	ctx := context.TODO()
   298  	pm.blobStore.Walk(ctx, func(info content.Info) error {
   299  		_, ok := used[info.Digest]
   300  		if ok {
   301  			return nil
   302  		}
   303  
   304  		return pm.blobStore.Delete(ctx, info.Digest)
   305  	})
   306  }
   307  
   308  type logHook struct{ id string }
   309  
   310  func (logHook) Levels() []log.Level {
   311  	return []log.Level{
   312  		log.PanicLevel,
   313  		log.FatalLevel,
   314  		log.ErrorLevel,
   315  		log.WarnLevel,
   316  		log.InfoLevel,
   317  		log.DebugLevel,
   318  		log.TraceLevel,
   319  	}
   320  }
   321  
   322  func (l logHook) Fire(entry *log.Entry) error {
   323  	entry.Data = log.Fields{"plugin": l.id}
   324  	return nil
   325  }
   326  
   327  func makeLoggerStreams(id string) (stdout, stderr io.WriteCloser) {
   328  	logger := logrus.New()
   329  	logger.Hooks.Add(logHook{id})
   330  	return logger.WriterLevel(log.InfoLevel), logger.WriterLevel(log.ErrorLevel)
   331  }
   332  
   333  func validatePrivileges(requiredPrivileges, privileges types.PluginPrivileges) error {
   334  	if !isEqual(requiredPrivileges, privileges, isEqualPrivilege) {
   335  		return errors.New("incorrect privileges")
   336  	}
   337  
   338  	return nil
   339  }
   340  
   341  func isEqual(arrOne, arrOther types.PluginPrivileges, compare func(x, y types.PluginPrivilege) bool) bool {
   342  	if len(arrOne) != len(arrOther) {
   343  		return false
   344  	}
   345  
   346  	sort.Sort(arrOne)
   347  	sort.Sort(arrOther)
   348  
   349  	for i := 1; i < arrOne.Len(); i++ {
   350  		if !compare(arrOne[i], arrOther[i]) {
   351  			return false
   352  		}
   353  	}
   354  
   355  	return true
   356  }
   357  
   358  func isEqualPrivilege(a, b types.PluginPrivilege) bool {
   359  	if a.Name != b.Name {
   360  		return false
   361  	}
   362  
   363  	return reflect.DeepEqual(a.Value, b.Value)
   364  }