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 }