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