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