Browse code

Move plugin to daemon/pkg/plugin

Signed-off-by: Derek McGowan <derek@mcg.dev>

Derek McGowan authored on 2025/06/28 06:26:33
Showing 41 changed files
... ...
@@ -12,8 +12,8 @@ import (
12 12
 	"github.com/docker/docker/api/types/backend"
13 13
 	"github.com/docker/docker/api/types/registry"
14 14
 	"github.com/docker/docker/api/types/swarm/runtime"
15
+	"github.com/docker/docker/daemon/pkg/plugin"
15 16
 	v2 "github.com/docker/docker/daemon/pkg/plugin/v2"
16
-	"github.com/docker/docker/plugin"
17 17
 	"github.com/gogo/protobuf/proto"
18 18
 	"github.com/moby/swarmkit/v2/api"
19 19
 	"github.com/pkg/errors"
... ...
@@ -15,8 +15,8 @@ import (
15 15
 	"github.com/docker/docker/api/types/backend"
16 16
 	"github.com/docker/docker/api/types/registry"
17 17
 	"github.com/docker/docker/api/types/swarm/runtime"
18
+	"github.com/docker/docker/daemon/pkg/plugin"
18 19
 	v2 "github.com/docker/docker/daemon/pkg/plugin/v2"
19
-	"github.com/docker/docker/plugin"
20 20
 	"github.com/moby/pubsub"
21 21
 	"github.com/sirupsen/logrus"
22 22
 )
... ...
@@ -18,11 +18,11 @@ import (
18 18
 	"github.com/docker/docker/api/types/volume"
19 19
 	clustertypes "github.com/docker/docker/daemon/cluster/provider"
20 20
 	networkSettings "github.com/docker/docker/daemon/network"
21
+	"github.com/docker/docker/daemon/pkg/plugin"
21 22
 	"github.com/docker/docker/image"
22 23
 	"github.com/docker/docker/libnetwork"
23 24
 	"github.com/docker/docker/libnetwork/cluster"
24 25
 	networktypes "github.com/docker/docker/libnetwork/types"
25
-	"github.com/docker/docker/plugin"
26 26
 	volumeopts "github.com/docker/docker/volume/service/opts"
27 27
 	"github.com/moby/swarmkit/v2/agent/exec"
28 28
 	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
... ...
@@ -29,6 +29,7 @@ import (
29 29
 	"github.com/docker/docker/daemon/command/trap"
30 30
 	"github.com/docker/docker/daemon/config"
31 31
 	"github.com/docker/docker/daemon/listeners"
32
+	"github.com/docker/docker/daemon/pkg/plugin"
32 33
 	apiserver "github.com/docker/docker/daemon/server"
33 34
 	"github.com/docker/docker/daemon/server/middleware"
34 35
 	"github.com/docker/docker/daemon/server/router"
... ...
@@ -55,7 +56,6 @@ import (
55 55
 	"github.com/docker/docker/pkg/plugingetter"
56 56
 	"github.com/docker/docker/pkg/rootless"
57 57
 	"github.com/docker/docker/pkg/sysinfo"
58
-	"github.com/docker/docker/plugin"
59 58
 	"github.com/docker/docker/runconfig"
60 59
 	"github.com/docker/go-connections/tlsconfig"
61 60
 	"github.com/moby/buildkit/session"
... ...
@@ -50,6 +50,7 @@ import (
50 50
 	pluginexec "github.com/docker/docker/daemon/internal/plugin/executor/containerd"
51 51
 	dlogger "github.com/docker/docker/daemon/logger"
52 52
 	"github.com/docker/docker/daemon/network"
53
+	"github.com/docker/docker/daemon/pkg/plugin"
53 54
 	"github.com/docker/docker/daemon/snapshotter"
54 55
 	"github.com/docker/docker/daemon/stats"
55 56
 	"github.com/docker/docker/distribution"
... ...
@@ -68,7 +69,6 @@ import (
68 68
 	"github.com/docker/docker/pkg/idtools"
69 69
 	"github.com/docker/docker/pkg/plugingetter"
70 70
 	"github.com/docker/docker/pkg/sysinfo"
71
-	"github.com/docker/docker/plugin"
72 71
 	refstore "github.com/docker/docker/reference"
73 72
 	"github.com/docker/docker/registry"
74 73
 	volumesservice "github.com/docker/docker/volume/service"
... ...
@@ -12,9 +12,9 @@ import (
12 12
 	"time"
13 13
 
14 14
 	"github.com/containerd/log"
15
+	"github.com/docker/docker/daemon/pkg/plugin"
15 16
 	"github.com/docker/docker/pkg/plugingetter"
16 17
 	"github.com/docker/docker/pkg/plugins"
17
-	"github.com/docker/docker/plugin"
18 18
 	gometrics "github.com/docker/go-metrics"
19 19
 	"github.com/opencontainers/runtime-spec/specs-go"
20 20
 	"github.com/pkg/errors"
... ...
@@ -3,8 +3,8 @@
3 3
 package metrics
4 4
 
5 5
 import (
6
+	"github.com/docker/docker/daemon/pkg/plugin"
6 7
 	"github.com/docker/docker/pkg/plugingetter"
7
-	"github.com/docker/docker/plugin"
8 8
 )
9 9
 
10 10
 func RegisterPlugin(*plugin.Store, string) error { return nil }
11 11
new file mode 100644
... ...
@@ -0,0 +1,836 @@
0
+package plugin
1
+
2
+import (
3
+	"archive/tar"
4
+	"bytes"
5
+	"compress/gzip"
6
+	"context"
7
+	"encoding/json"
8
+	"io"
9
+	"net/http"
10
+	"os"
11
+	"path"
12
+	"path/filepath"
13
+	"strings"
14
+	"time"
15
+
16
+	"github.com/containerd/containerd/v2/core/content"
17
+	c8dimages "github.com/containerd/containerd/v2/core/images"
18
+	"github.com/containerd/containerd/v2/core/remotes"
19
+	"github.com/containerd/containerd/v2/core/remotes/docker"
20
+	"github.com/containerd/log"
21
+	"github.com/containerd/platforms"
22
+	"github.com/distribution/reference"
23
+	"github.com/docker/distribution/manifest/schema2"
24
+	"github.com/docker/docker/api/types"
25
+	"github.com/docker/docker/api/types/backend"
26
+	"github.com/docker/docker/api/types/events"
27
+	"github.com/docker/docker/api/types/filters"
28
+	"github.com/docker/docker/api/types/registry"
29
+	v2 "github.com/docker/docker/daemon/pkg/plugin/v2"
30
+	"github.com/docker/docker/dockerversion"
31
+	"github.com/docker/docker/errdefs"
32
+	"github.com/docker/docker/internal/containerfs"
33
+	"github.com/docker/docker/pkg/authorization"
34
+	"github.com/docker/docker/pkg/pools"
35
+	"github.com/docker/docker/pkg/progress"
36
+	"github.com/docker/docker/pkg/stringid"
37
+	"github.com/moby/go-archive/chrootarchive"
38
+	"github.com/moby/sys/mount"
39
+	"github.com/opencontainers/go-digest"
40
+	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
41
+	"github.com/pkg/errors"
42
+)
43
+
44
+var acceptedPluginFilterTags = map[string]bool{
45
+	"enabled":    true,
46
+	"capability": true,
47
+}
48
+
49
+// Disable deactivates a plugin. This means resources (volumes, networks) cant use them.
50
+func (pm *Manager) Disable(refOrID string, config *backend.PluginDisableConfig) error {
51
+	p, err := pm.config.Store.GetV2Plugin(refOrID)
52
+	if err != nil {
53
+		return err
54
+	}
55
+	pm.mu.RLock()
56
+	c := pm.cMap[p]
57
+	pm.mu.RUnlock()
58
+
59
+	if !config.ForceDisable && p.GetRefCount() > 0 {
60
+		return errors.WithStack(inUseError(p.Name()))
61
+	}
62
+
63
+	for _, typ := range p.GetTypes() {
64
+		if typ.Capability == authorization.AuthZApiImplements {
65
+			pm.config.AuthzMiddleware.RemovePlugin(p.Name())
66
+		}
67
+	}
68
+
69
+	if err := pm.disable(p, c); err != nil {
70
+		return err
71
+	}
72
+	pm.publisher.Publish(EventDisable{Plugin: p.PluginObj})
73
+	pm.config.LogPluginEvent(p.GetID(), refOrID, events.ActionDisable)
74
+	return nil
75
+}
76
+
77
+// Enable activates a plugin, which implies that they are ready to be used by containers.
78
+func (pm *Manager) Enable(refOrID string, config *backend.PluginEnableConfig) error {
79
+	p, err := pm.config.Store.GetV2Plugin(refOrID)
80
+	if err != nil {
81
+		return err
82
+	}
83
+
84
+	c := &controller{timeoutInSecs: config.Timeout}
85
+	if err := pm.enable(p, c, false); err != nil {
86
+		return err
87
+	}
88
+	pm.publisher.Publish(EventEnable{Plugin: p.PluginObj})
89
+	pm.config.LogPluginEvent(p.GetID(), refOrID, events.ActionEnable)
90
+	return nil
91
+}
92
+
93
+// Inspect examines a plugin config
94
+func (pm *Manager) Inspect(refOrID string) (*types.Plugin, error) {
95
+	p, err := pm.config.Store.GetV2Plugin(refOrID)
96
+	if err != nil {
97
+		return nil, err
98
+	}
99
+
100
+	return &p.PluginObj, nil
101
+}
102
+
103
+func computePrivileges(c types.PluginConfig) types.PluginPrivileges {
104
+	var privileges types.PluginPrivileges
105
+	if c.Network.Type != "null" && c.Network.Type != "bridge" && c.Network.Type != "" {
106
+		privileges = append(privileges, types.PluginPrivilege{
107
+			Name:        "network",
108
+			Description: "permissions to access a network",
109
+			Value:       []string{c.Network.Type},
110
+		})
111
+	}
112
+	if c.IpcHost {
113
+		privileges = append(privileges, types.PluginPrivilege{
114
+			Name:        "host ipc namespace",
115
+			Description: "allow access to host ipc namespace",
116
+			Value:       []string{"true"},
117
+		})
118
+	}
119
+	if c.PidHost {
120
+		privileges = append(privileges, types.PluginPrivilege{
121
+			Name:        "host pid namespace",
122
+			Description: "allow access to host pid namespace",
123
+			Value:       []string{"true"},
124
+		})
125
+	}
126
+	for _, mnt := range c.Mounts {
127
+		if mnt.Source != nil {
128
+			privileges = append(privileges, types.PluginPrivilege{
129
+				Name:        "mount",
130
+				Description: "host path to mount",
131
+				Value:       []string{*mnt.Source},
132
+			})
133
+		}
134
+	}
135
+	for _, device := range c.Linux.Devices {
136
+		if device.Path != nil {
137
+			privileges = append(privileges, types.PluginPrivilege{
138
+				Name:        "device",
139
+				Description: "host device to access",
140
+				Value:       []string{*device.Path},
141
+			})
142
+		}
143
+	}
144
+	if c.Linux.AllowAllDevices {
145
+		privileges = append(privileges, types.PluginPrivilege{
146
+			Name:        "allow-all-devices",
147
+			Description: "allow 'rwm' access to all devices",
148
+			Value:       []string{"true"},
149
+		})
150
+	}
151
+	if len(c.Linux.Capabilities) > 0 {
152
+		privileges = append(privileges, types.PluginPrivilege{
153
+			Name:        "capabilities",
154
+			Description: "list of additional capabilities required",
155
+			Value:       c.Linux.Capabilities,
156
+		})
157
+	}
158
+
159
+	return privileges
160
+}
161
+
162
+// Privileges pulls a plugin config and computes the privileges required to install it.
163
+func (pm *Manager) Privileges(ctx context.Context, ref reference.Named, metaHeader http.Header, authConfig *registry.AuthConfig) (types.PluginPrivileges, error) {
164
+	var (
165
+		config     types.PluginConfig
166
+		configSeen bool
167
+	)
168
+
169
+	h := func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
170
+		switch desc.MediaType {
171
+		case schema2.MediaTypeManifest, ocispec.MediaTypeImageManifest:
172
+			data, err := content.ReadBlob(ctx, pm.blobStore, desc)
173
+			if err != nil {
174
+				return nil, errors.Wrapf(err, "error reading image manifest from blob store for %s", ref)
175
+			}
176
+
177
+			var m ocispec.Manifest
178
+			if err := json.Unmarshal(data, &m); err != nil {
179
+				return nil, errors.Wrapf(err, "error unmarshaling image manifest for %s", ref)
180
+			}
181
+			return []ocispec.Descriptor{m.Config}, nil
182
+		case schema2.MediaTypePluginConfig:
183
+			configSeen = true
184
+			data, err := content.ReadBlob(ctx, pm.blobStore, desc)
185
+			if err != nil {
186
+				return nil, errors.Wrapf(err, "error reading plugin config from blob store for %s", ref)
187
+			}
188
+
189
+			if err := json.Unmarshal(data, &config); err != nil {
190
+				return nil, errors.Wrapf(err, "error unmarshaling plugin config for %s", ref)
191
+			}
192
+		}
193
+
194
+		return nil, nil
195
+	}
196
+
197
+	if err := pm.fetch(ctx, ref, authConfig, progress.DiscardOutput(), metaHeader, c8dimages.HandlerFunc(h)); err != nil {
198
+		return types.PluginPrivileges{}, nil
199
+	}
200
+
201
+	if !configSeen {
202
+		return types.PluginPrivileges{}, errors.Errorf("did not find plugin config for specified reference %s", ref)
203
+	}
204
+
205
+	return computePrivileges(config), nil
206
+}
207
+
208
+// Upgrade upgrades a plugin
209
+//
210
+// TODO: replace reference package usage with simpler url.Parse semantics
211
+func (pm *Manager) Upgrade(ctx context.Context, ref reference.Named, name string, metaHeader http.Header, authConfig *registry.AuthConfig, privileges types.PluginPrivileges, outStream io.Writer) error {
212
+	p, err := pm.config.Store.GetV2Plugin(name)
213
+	if err != nil {
214
+		return err
215
+	}
216
+
217
+	if p.IsEnabled() {
218
+		return errors.Wrap(enabledError(p.Name()), "plugin must be disabled before upgrading")
219
+	}
220
+
221
+	// revalidate because Pull is public
222
+	if _, err := reference.ParseNormalizedNamed(name); err != nil {
223
+		return errors.Wrapf(errdefs.InvalidParameter(err), "failed to parse %q", name)
224
+	}
225
+
226
+	pm.muGC.RLock()
227
+	defer pm.muGC.RUnlock()
228
+
229
+	tmpRootFSDir, err := os.MkdirTemp(pm.tmpDir(), ".rootfs")
230
+	if err != nil {
231
+		return errors.Wrap(err, "error creating tmp dir for plugin rootfs")
232
+	}
233
+
234
+	var md fetchMeta
235
+
236
+	ctx, cancel := context.WithCancel(ctx)
237
+	out, waitProgress := setupProgressOutput(outStream, cancel)
238
+	defer waitProgress()
239
+
240
+	if err := pm.fetch(ctx, ref, authConfig, out, metaHeader, storeFetchMetadata(&md), childrenHandler(pm.blobStore), applyLayer(pm.blobStore, tmpRootFSDir, out)); err != nil {
241
+		return err
242
+	}
243
+	pm.config.LogPluginEvent(reference.FamiliarString(ref), name, events.ActionPull)
244
+
245
+	if err := validateFetchedMetadata(md); err != nil {
246
+		return err
247
+	}
248
+
249
+	if err := pm.upgradePlugin(p, md.config, md.manifest, md.blobs, tmpRootFSDir, &privileges); err != nil {
250
+		return err
251
+	}
252
+	p.PluginObj.PluginReference = ref.String()
253
+	return nil
254
+}
255
+
256
+// Pull pulls a plugin, check if the correct privileges are provided and install the plugin.
257
+//
258
+// TODO: replace reference package usage with simpler url.Parse semantics
259
+func (pm *Manager) Pull(ctx context.Context, ref reference.Named, name string, metaHeader http.Header, authConfig *registry.AuthConfig, privileges types.PluginPrivileges, outStream io.Writer, opts ...CreateOpt) error {
260
+	pm.muGC.RLock()
261
+	defer pm.muGC.RUnlock()
262
+
263
+	// revalidate because Pull is public
264
+	nameref, err := reference.ParseNormalizedNamed(name)
265
+	if err != nil {
266
+		return errors.Wrapf(errdefs.InvalidParameter(err), "failed to parse %q", name)
267
+	}
268
+	name = reference.FamiliarString(reference.TagNameOnly(nameref))
269
+
270
+	if err := pm.config.Store.validateName(name); err != nil {
271
+		return errdefs.InvalidParameter(err)
272
+	}
273
+
274
+	tmpRootFSDir, err := os.MkdirTemp(pm.tmpDir(), ".rootfs")
275
+	if err != nil {
276
+		return errors.Wrap(errdefs.System(err), "error preparing upgrade")
277
+	}
278
+	defer os.RemoveAll(tmpRootFSDir)
279
+
280
+	var md fetchMeta
281
+
282
+	ctx, cancel := context.WithCancel(ctx)
283
+	out, waitProgress := setupProgressOutput(outStream, cancel)
284
+	defer waitProgress()
285
+
286
+	if err := pm.fetch(ctx, ref, authConfig, out, metaHeader, storeFetchMetadata(&md), childrenHandler(pm.blobStore), applyLayer(pm.blobStore, tmpRootFSDir, out)); err != nil {
287
+		return err
288
+	}
289
+	pm.config.LogPluginEvent(reference.FamiliarString(ref), name, events.ActionPull)
290
+
291
+	if err := validateFetchedMetadata(md); err != nil {
292
+		return err
293
+	}
294
+
295
+	refOpt := func(p *v2.Plugin) {
296
+		p.PluginObj.PluginReference = ref.String()
297
+	}
298
+	optsList := make([]CreateOpt, 0, len(opts)+1)
299
+	optsList = append(optsList, opts...)
300
+	optsList = append(optsList, refOpt)
301
+
302
+	// TODO: tmpRootFSDir is empty but should have layers in it
303
+	p, err := pm.createPlugin(name, md.config, md.manifest, md.blobs, tmpRootFSDir, &privileges, optsList...)
304
+	if err != nil {
305
+		return err
306
+	}
307
+
308
+	pm.publisher.Publish(EventCreate{Plugin: p.PluginObj})
309
+
310
+	return nil
311
+}
312
+
313
+// List displays the list of plugins and associated metadata.
314
+func (pm *Manager) List(pluginFilters filters.Args) ([]types.Plugin, error) {
315
+	if err := pluginFilters.Validate(acceptedPluginFilterTags); err != nil {
316
+		return nil, err
317
+	}
318
+
319
+	enabledOnly := false
320
+	disabledOnly := false
321
+	if pluginFilters.Contains("enabled") {
322
+		enabledFilter, err := pluginFilters.GetBoolOrDefault("enabled", false)
323
+		if err != nil {
324
+			return nil, err
325
+		}
326
+
327
+		if enabledFilter {
328
+			enabledOnly = true
329
+		} else {
330
+			disabledOnly = true
331
+		}
332
+	}
333
+
334
+	plugins := pm.config.Store.GetAll()
335
+	out := make([]types.Plugin, 0, len(plugins))
336
+
337
+next:
338
+	for _, p := range plugins {
339
+		if enabledOnly && !p.PluginObj.Enabled {
340
+			continue
341
+		}
342
+		if disabledOnly && p.PluginObj.Enabled {
343
+			continue
344
+		}
345
+		if pluginFilters.Contains("capability") {
346
+			for _, f := range p.GetTypes() {
347
+				if !pluginFilters.Match("capability", f.Capability) {
348
+					continue next
349
+				}
350
+			}
351
+		}
352
+		out = append(out, p.PluginObj)
353
+	}
354
+	return out, nil
355
+}
356
+
357
+// Push pushes a plugin to the registry.
358
+func (pm *Manager) Push(ctx context.Context, name string, metaHeader http.Header, authConfig *registry.AuthConfig, outStream io.Writer) error {
359
+	p, err := pm.config.Store.GetV2Plugin(name)
360
+	if err != nil {
361
+		return err
362
+	}
363
+
364
+	ref, err := reference.ParseNormalizedNamed(p.Name())
365
+	if err != nil {
366
+		return errors.Wrapf(err, "plugin has invalid name %v for push", p.Name())
367
+	}
368
+
369
+	statusTracker := docker.NewInMemoryTracker()
370
+
371
+	resolver, err := pm.newResolver(ctx, statusTracker, authConfig, metaHeader, false)
372
+	if err != nil {
373
+		return err
374
+	}
375
+
376
+	pusher, err := resolver.Pusher(ctx, ref.String())
377
+	if err != nil {
378
+		return errors.Wrap(err, "error creating plugin pusher")
379
+	}
380
+
381
+	pj := newPushJobs(statusTracker)
382
+
383
+	ctx, cancel := context.WithCancel(ctx)
384
+	out, waitProgress := setupProgressOutput(outStream, cancel)
385
+	defer waitProgress()
386
+
387
+	progressHandler := c8dimages.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
388
+		log.G(ctx).WithField("mediaType", desc.MediaType).WithField("digest", desc.Digest.String()).Debug("Preparing to push plugin layer")
389
+		id := stringid.TruncateID(desc.Digest.String())
390
+		pj.add(remotes.MakeRefKey(ctx, desc), id)
391
+		progress.Update(out, id, "Preparing")
392
+		return nil, nil
393
+	})
394
+
395
+	desc, err := pm.getManifestDescriptor(ctx, p)
396
+	if err != nil {
397
+		return errors.Wrap(err, "error reading plugin manifest")
398
+	}
399
+
400
+	progress.Messagef(out, "", "The push refers to repository [%s]", reference.FamiliarName(ref))
401
+
402
+	// TODO: If a layer already exists on the registry, the progress output just says "Preparing"
403
+	go func() {
404
+		timer := time.NewTimer(100 * time.Millisecond)
405
+		defer timer.Stop()
406
+		if !timer.Stop() {
407
+			<-timer.C
408
+		}
409
+		var statuses []contentStatus
410
+		for {
411
+			timer.Reset(100 * time.Millisecond)
412
+			select {
413
+			case <-ctx.Done():
414
+				return
415
+			case <-timer.C:
416
+				statuses = pj.status()
417
+			}
418
+
419
+			for _, s := range statuses {
420
+				out.WriteProgress(progress.Progress{ID: s.Ref, Current: s.Offset, Total: s.Total, Action: s.Status, LastUpdate: s.Offset == s.Total})
421
+			}
422
+		}
423
+	}()
424
+
425
+	// Make sure we can authenticate the request since the auth scope for plugin repos is different than a normal repo.
426
+	ctx = docker.WithScope(ctx, scope(ref, true))
427
+	if err := remotes.PushContent(ctx, pusher, desc, pm.blobStore, nil, nil, func(h c8dimages.Handler) c8dimages.Handler {
428
+		return c8dimages.Handlers(progressHandler, h)
429
+	}); err != nil {
430
+		// Try fallback to http.
431
+		// This is needed because the containerd pusher will only attempt the first registry config we pass, which would
432
+		// typically be https.
433
+		// If there are no http-only host configs found we'll error out anyway.
434
+		resolver, _ := pm.newResolver(ctx, statusTracker, authConfig, metaHeader, true)
435
+		if resolver != nil {
436
+			pusher, _ := resolver.Pusher(ctx, ref.String())
437
+			if pusher != nil {
438
+				log.G(ctx).WithField("ref", ref).Debug("Re-attmpting push with http-fallback")
439
+				err2 := remotes.PushContent(ctx, pusher, desc, pm.blobStore, nil, nil, func(h c8dimages.Handler) c8dimages.Handler {
440
+					return c8dimages.Handlers(progressHandler, h)
441
+				})
442
+				if err2 == nil {
443
+					err = nil
444
+				} else {
445
+					log.G(ctx).WithError(err2).WithField("ref", ref).Debug("Error while attempting push with http-fallback")
446
+				}
447
+			}
448
+		}
449
+		if err != nil {
450
+			return errors.Wrap(err, "error pushing plugin")
451
+		}
452
+	}
453
+
454
+	// For blobs that already exist in the registry we need to make sure to update the progress otherwise it will just say "pending"
455
+	// TODO: How to check if the layer already exists? Is it worth it?
456
+	for _, j := range pj.jobs {
457
+		progress.Update(out, pj.names[j], "Upload complete")
458
+	}
459
+
460
+	// Signal the client for content trust verification
461
+	progress.Aux(out, types.PushResult{Tag: ref.(reference.Tagged).Tag(), Digest: desc.Digest.String(), Size: int(desc.Size)})
462
+
463
+	return nil
464
+}
465
+
466
+// manifest wraps an OCI manifest, because...
467
+// Historically the registry does not support plugins unless the media type on the manifest is specifically schema2.MediaTypeManifest
468
+// So the OCI manifest media type is not supported.
469
+// Additionally, there is extra validation for the docker schema2 manifest than there is a mediatype set on the manifest itself
470
+// even though this is set on the descriptor
471
+// The OCI types do not have this field.
472
+type manifest struct {
473
+	ocispec.Manifest
474
+	MediaType string `json:"mediaType,omitempty"`
475
+}
476
+
477
+func buildManifest(ctx context.Context, s content.Manager, config digest.Digest, layers []digest.Digest) (manifest, error) {
478
+	var m manifest
479
+	m.MediaType = c8dimages.MediaTypeDockerSchema2Manifest
480
+	m.SchemaVersion = 2
481
+
482
+	configInfo, err := s.Info(ctx, config)
483
+	if err != nil {
484
+		return m, errors.Wrapf(err, "error reading plugin config content for digest %s", config)
485
+	}
486
+	m.Config = ocispec.Descriptor{
487
+		MediaType: mediaTypePluginConfig,
488
+		Size:      configInfo.Size,
489
+		Digest:    configInfo.Digest,
490
+	}
491
+
492
+	for _, l := range layers {
493
+		info, err := s.Info(ctx, l)
494
+		if err != nil {
495
+			return m, errors.Wrapf(err, "error fetching info for content digest %s", l)
496
+		}
497
+		m.Layers = append(m.Layers, ocispec.Descriptor{
498
+			MediaType: c8dimages.MediaTypeDockerSchema2LayerGzip, // TODO: This is assuming everything is a gzip compressed layer, but that may not be true.
499
+			Digest:    l,
500
+			Size:      info.Size,
501
+		})
502
+	}
503
+	return m, nil
504
+}
505
+
506
+// getManifestDescriptor gets the OCI descriptor for a manifest
507
+// It will generate a manifest if one does not exist
508
+func (pm *Manager) getManifestDescriptor(ctx context.Context, p *v2.Plugin) (ocispec.Descriptor, error) {
509
+	logger := log.G(ctx).WithField("plugin", p.Name()).WithField("digest", p.Manifest)
510
+	if p.Manifest != "" {
511
+		info, err := pm.blobStore.Info(ctx, p.Manifest)
512
+		if err == nil {
513
+			desc := ocispec.Descriptor{
514
+				Size:      info.Size,
515
+				Digest:    info.Digest,
516
+				MediaType: c8dimages.MediaTypeDockerSchema2Manifest,
517
+			}
518
+			return desc, nil
519
+		}
520
+		logger.WithError(err).Debug("Could not find plugin manifest in content store")
521
+	} else {
522
+		logger.Info("Plugin does not have manifest digest")
523
+	}
524
+	logger.Info("Building a new plugin manifest")
525
+
526
+	mfst, err := buildManifest(ctx, pm.blobStore, p.Config, p.Blobsums)
527
+	if err != nil {
528
+		return ocispec.Descriptor{}, err
529
+	}
530
+
531
+	desc, err := writeManifest(ctx, pm.blobStore, &mfst)
532
+	if err != nil {
533
+		return desc, err
534
+	}
535
+
536
+	if err := pm.save(p); err != nil {
537
+		logger.WithError(err).Error("Could not save plugin with manifest digest")
538
+	}
539
+	return desc, nil
540
+}
541
+
542
+func writeManifest(ctx context.Context, cs content.Store, m *manifest) (ocispec.Descriptor, error) {
543
+	platform := platforms.DefaultSpec()
544
+	desc := ocispec.Descriptor{
545
+		MediaType: c8dimages.MediaTypeDockerSchema2Manifest,
546
+		Platform:  &platform,
547
+	}
548
+	data, err := json.Marshal(m)
549
+	if err != nil {
550
+		return desc, errors.Wrap(err, "error encoding manifest")
551
+	}
552
+	desc.Digest = digest.FromBytes(data)
553
+	desc.Size = int64(len(data))
554
+
555
+	if err := content.WriteBlob(ctx, cs, remotes.MakeRefKey(ctx, desc), bytes.NewReader(data), desc); err != nil {
556
+		return desc, errors.Wrap(err, "error writing plugin manifest")
557
+	}
558
+	return desc, nil
559
+}
560
+
561
+// Remove deletes plugin's root directory.
562
+func (pm *Manager) Remove(name string, config *backend.PluginRmConfig) error {
563
+	p, err := pm.config.Store.GetV2Plugin(name)
564
+	pm.mu.RLock()
565
+	c := pm.cMap[p]
566
+	pm.mu.RUnlock()
567
+
568
+	if err != nil {
569
+		return err
570
+	}
571
+
572
+	if !config.ForceRemove {
573
+		if p.GetRefCount() > 0 {
574
+			return inUseError(p.Name())
575
+		}
576
+		if p.IsEnabled() {
577
+			return enabledError(p.Name())
578
+		}
579
+	}
580
+
581
+	if p.IsEnabled() {
582
+		if err := pm.disable(p, c); err != nil {
583
+			log.G(context.TODO()).Errorf("failed to disable plugin '%s': %s", p.Name(), err)
584
+		}
585
+	}
586
+
587
+	defer func() {
588
+		go pm.GC()
589
+	}()
590
+
591
+	id := p.GetID()
592
+	pluginDir := filepath.Join(pm.config.Root, id)
593
+
594
+	if err := mount.RecursiveUnmount(pluginDir); err != nil {
595
+		return errors.Wrap(err, "error unmounting plugin data")
596
+	}
597
+
598
+	if err := atomicRemoveAll(pluginDir); err != nil {
599
+		return err
600
+	}
601
+
602
+	pm.config.Store.Remove(p)
603
+	pm.config.LogPluginEvent(id, name, events.ActionRemove)
604
+	pm.publisher.Publish(EventRemove{Plugin: p.PluginObj})
605
+	return nil
606
+}
607
+
608
+// Set sets plugin args
609
+func (pm *Manager) Set(name string, args []string) error {
610
+	p, err := pm.config.Store.GetV2Plugin(name)
611
+	if err != nil {
612
+		return err
613
+	}
614
+	if err := p.Set(args); err != nil {
615
+		return err
616
+	}
617
+	return pm.save(p)
618
+}
619
+
620
+// CreateFromContext creates a plugin from the given pluginDir which contains
621
+// both the rootfs and the config.json and a repoName with optional tag.
622
+func (pm *Manager) CreateFromContext(ctx context.Context, tarCtx io.ReadCloser, options *types.PluginCreateOptions) (retErr error) {
623
+	pm.muGC.RLock()
624
+	defer pm.muGC.RUnlock()
625
+
626
+	ref, err := reference.ParseNormalizedNamed(options.RepoName)
627
+	if err != nil {
628
+		return errors.Wrapf(err, "failed to parse reference %v", options.RepoName)
629
+	}
630
+	if _, ok := ref.(reference.Canonical); ok {
631
+		return errors.Errorf("canonical references are not permitted")
632
+	}
633
+	name := reference.FamiliarString(reference.TagNameOnly(ref))
634
+
635
+	if err := pm.config.Store.validateName(name); err != nil { // fast check, real check is in createPlugin()
636
+		return err
637
+	}
638
+
639
+	tmpRootFSDir, err := os.MkdirTemp(pm.tmpDir(), ".rootfs")
640
+	if err != nil {
641
+		return errors.Wrap(err, "failed to create temp directory")
642
+	}
643
+	defer func() {
644
+		if err := os.RemoveAll(tmpRootFSDir); err != nil {
645
+			log.G(ctx).WithError(err).Warn("failed to remove temp rootfs directory")
646
+		}
647
+	}()
648
+
649
+	var configJSON []byte
650
+	rootFS := splitConfigRootFSFromTar(tarCtx, &configJSON)
651
+
652
+	rootFSBlob, err := pm.blobStore.Writer(ctx, content.WithRef(name))
653
+	if err != nil {
654
+		return err
655
+	}
656
+	defer rootFSBlob.Close()
657
+
658
+	gzw := gzip.NewWriter(rootFSBlob)
659
+	rootFSReader := io.TeeReader(rootFS, gzw)
660
+
661
+	if err := chrootarchive.Untar(rootFSReader, tmpRootFSDir, nil); err != nil {
662
+		return err
663
+	}
664
+	if err := rootFS.Close(); err != nil {
665
+		return err
666
+	}
667
+
668
+	if configJSON == nil {
669
+		return errors.New("config not found")
670
+	}
671
+
672
+	if err := gzw.Close(); err != nil {
673
+		return errors.Wrap(err, "error closing gzip writer")
674
+	}
675
+
676
+	var config types.PluginConfig
677
+	if err := json.Unmarshal(configJSON, &config); err != nil {
678
+		return errors.Wrap(err, "failed to parse config")
679
+	}
680
+
681
+	if err := pm.validateConfig(config); err != nil {
682
+		return err
683
+	}
684
+
685
+	pm.mu.Lock()
686
+	defer pm.mu.Unlock()
687
+
688
+	if err := rootFSBlob.Commit(ctx, 0, ""); err != nil {
689
+		return err
690
+	}
691
+	defer func() {
692
+		if retErr != nil {
693
+			go pm.GC()
694
+		}
695
+	}()
696
+
697
+	config.Rootfs = &types.PluginConfigRootfs{
698
+		Type:    "layers",
699
+		DiffIds: []string{rootFSBlob.Digest().String()},
700
+	}
701
+
702
+	config.DockerVersion = dockerversion.Version
703
+
704
+	configBlob, err := pm.blobStore.Writer(ctx, content.WithRef(name+"-config.json"))
705
+	if err != nil {
706
+		return err
707
+	}
708
+	defer configBlob.Close()
709
+	if err := json.NewEncoder(configBlob).Encode(config); err != nil {
710
+		return errors.Wrap(err, "error encoding json config")
711
+	}
712
+	if err := configBlob.Commit(ctx, 0, ""); err != nil {
713
+		return err
714
+	}
715
+
716
+	configDigest := configBlob.Digest()
717
+	layers := []digest.Digest{rootFSBlob.Digest()}
718
+
719
+	mfst, err := buildManifest(ctx, pm.blobStore, configDigest, layers)
720
+	if err != nil {
721
+		return err
722
+	}
723
+	desc, err := writeManifest(ctx, pm.blobStore, &mfst)
724
+	if err != nil {
725
+		return err
726
+	}
727
+
728
+	p, err := pm.createPlugin(name, configDigest, desc.Digest, layers, tmpRootFSDir, nil)
729
+	if err != nil {
730
+		return err
731
+	}
732
+	p.PluginObj.PluginReference = name
733
+
734
+	pm.publisher.Publish(EventCreate{Plugin: p.PluginObj})
735
+	pm.config.LogPluginEvent(p.PluginObj.ID, name, events.ActionCreate)
736
+
737
+	return nil
738
+}
739
+
740
+func (pm *Manager) validateConfig(config types.PluginConfig) error {
741
+	return nil // TODO:
742
+}
743
+
744
+func splitConfigRootFSFromTar(in io.ReadCloser, config *[]byte) io.ReadCloser {
745
+	pr, pw := io.Pipe()
746
+	go func() {
747
+		tarReader := tar.NewReader(in)
748
+		tarWriter := tar.NewWriter(pw)
749
+		defer in.Close()
750
+
751
+		hasRootFS := false
752
+
753
+		for {
754
+			hdr, err := tarReader.Next()
755
+			if err == io.EOF {
756
+				if !hasRootFS {
757
+					pw.CloseWithError(errors.Wrap(err, "no rootfs found"))
758
+					return
759
+				}
760
+				// Signals end of archive.
761
+				tarWriter.Close()
762
+				pw.Close()
763
+				return
764
+			}
765
+			if err != nil {
766
+				pw.CloseWithError(errors.Wrap(err, "failed to read from tar"))
767
+				return
768
+			}
769
+
770
+			content := io.Reader(tarReader)
771
+			name := path.Clean(hdr.Name)
772
+			if path.IsAbs(name) {
773
+				name = name[1:]
774
+			}
775
+			if name == configFileName {
776
+				dt, err := io.ReadAll(content)
777
+				if err != nil {
778
+					pw.CloseWithError(errors.Wrapf(err, "failed to read %s", configFileName))
779
+					return
780
+				}
781
+				*config = dt
782
+			}
783
+			if parts := strings.Split(name, "/"); len(parts) != 0 && parts[0] == rootFSFileName {
784
+				hdr.Name = path.Clean(path.Join(parts[1:]...))
785
+				if hdr.Typeflag == tar.TypeLink && strings.HasPrefix(strings.ToLower(hdr.Linkname), rootFSFileName+"/") {
786
+					hdr.Linkname = hdr.Linkname[len(rootFSFileName)+1:]
787
+				}
788
+				if err := tarWriter.WriteHeader(hdr); err != nil {
789
+					pw.CloseWithError(errors.Wrap(err, "error writing tar header"))
790
+					return
791
+				}
792
+				if _, err := pools.Copy(tarWriter, content); err != nil {
793
+					pw.CloseWithError(errors.Wrap(err, "error copying tar data"))
794
+					return
795
+				}
796
+				hasRootFS = true
797
+			} else {
798
+				io.Copy(io.Discard, content)
799
+			}
800
+		}
801
+	}()
802
+	return pr
803
+}
804
+
805
+func atomicRemoveAll(dir string) error {
806
+	renamed := dir + "-removing"
807
+
808
+	err := os.Rename(dir, renamed)
809
+	switch {
810
+	case os.IsNotExist(err), err == nil:
811
+		// even if `dir` doesn't exist, we can still try and remove `renamed`
812
+	case os.IsExist(err):
813
+		// Some previous remove failed, check if the origin dir exists
814
+		if e := containerfs.EnsureRemoveAll(renamed); e != nil {
815
+			return errors.Wrap(err, "rename target already exists and could not be removed")
816
+		}
817
+		if _, err := os.Stat(dir); os.IsNotExist(err) {
818
+			// origin doesn't exist, nothing left to do
819
+			return nil
820
+		}
821
+
822
+		// attempt to rename again
823
+		if err := os.Rename(dir, renamed); err != nil {
824
+			return errors.Wrap(err, "failed to rename dir for atomic removal")
825
+		}
826
+	default:
827
+		return errors.Wrap(err, "failed to rename dir for atomic removal")
828
+	}
829
+
830
+	if err := containerfs.EnsureRemoveAll(renamed); err != nil {
831
+		os.Rename(renamed, dir)
832
+		return err
833
+	}
834
+	return nil
835
+}
0 836
new file mode 100644
... ...
@@ -0,0 +1,68 @@
0
+package plugin
1
+
2
+import (
3
+	"os"
4
+	"path/filepath"
5
+	"testing"
6
+)
7
+
8
+func TestAtomicRemoveAllNormal(t *testing.T) {
9
+	dir := t.TempDir()
10
+
11
+	if err := atomicRemoveAll(dir); err != nil {
12
+		t.Fatal(err)
13
+	}
14
+
15
+	if _, err := os.Stat(dir); !os.IsNotExist(err) {
16
+		t.Fatalf("dir should be gone: %v", err)
17
+	}
18
+	if _, err := os.Stat(dir + "-removing"); !os.IsNotExist(err) {
19
+		t.Fatalf("dir should be gone: %v", err)
20
+	}
21
+}
22
+
23
+func TestAtomicRemoveAllAlreadyExists(t *testing.T) {
24
+	dir := t.TempDir()
25
+
26
+	if err := os.MkdirAll(dir+"-removing", 0o755); err != nil {
27
+		t.Fatal(err)
28
+	}
29
+	defer os.RemoveAll(dir + "-removing")
30
+
31
+	if err := atomicRemoveAll(dir); err != nil {
32
+		t.Fatal(err)
33
+	}
34
+
35
+	if _, err := os.Stat(dir); !os.IsNotExist(err) {
36
+		t.Fatalf("dir should be gone: %v", err)
37
+	}
38
+	if _, err := os.Stat(dir + "-removing"); !os.IsNotExist(err) {
39
+		t.Fatalf("dir should be gone: %v", err)
40
+	}
41
+}
42
+
43
+func TestAtomicRemoveAllNotExist(t *testing.T) {
44
+	if err := atomicRemoveAll("/not-exist"); err != nil {
45
+		t.Fatal(err)
46
+	}
47
+
48
+	dir := t.TempDir()
49
+
50
+	// create the removing dir, but not the "real" one
51
+	foo := filepath.Join(dir, "foo")
52
+	removing := dir + "-removing"
53
+	if err := os.MkdirAll(removing, 0o755); err != nil {
54
+		t.Fatal(err)
55
+	}
56
+
57
+	if err := atomicRemoveAll(dir); err != nil {
58
+		t.Fatal(err)
59
+	}
60
+
61
+	if _, err := os.Stat(foo); !os.IsNotExist(err) {
62
+		t.Fatalf("dir should be gone: %v", err)
63
+	}
64
+	if _, err := os.Stat(removing); !os.IsNotExist(err) {
65
+		t.Fatalf("dir should be gone: %v", err)
66
+	}
67
+}
0 68
new file mode 100644
... ...
@@ -0,0 +1,74 @@
0
+//go:build !linux
1
+
2
+package plugin
3
+
4
+import (
5
+	"context"
6
+	"errors"
7
+	"io"
8
+	"net/http"
9
+
10
+	"github.com/distribution/reference"
11
+	"github.com/docker/docker/api/types"
12
+	"github.com/docker/docker/api/types/backend"
13
+	"github.com/docker/docker/api/types/filters"
14
+	"github.com/docker/docker/api/types/registry"
15
+)
16
+
17
+var errNotSupported = errors.New("plugins are not supported on this platform")
18
+
19
+// Disable deactivates a plugin, which implies that they cannot be used by containers.
20
+func (pm *Manager) Disable(name string, config *backend.PluginDisableConfig) error {
21
+	return errNotSupported
22
+}
23
+
24
+// Enable activates a plugin, which implies that they are ready to be used by containers.
25
+func (pm *Manager) Enable(name string, config *backend.PluginEnableConfig) error {
26
+	return errNotSupported
27
+}
28
+
29
+// Inspect examines a plugin config
30
+func (pm *Manager) Inspect(refOrID string) (*types.Plugin, error) {
31
+	return nil, errNotSupported
32
+}
33
+
34
+// Privileges pulls a plugin config and computes the privileges required to install it.
35
+func (pm *Manager) Privileges(ctx context.Context, ref reference.Named, metaHeader http.Header, authConfig *registry.AuthConfig) (types.PluginPrivileges, error) {
36
+	return nil, errNotSupported
37
+}
38
+
39
+// Pull pulls a plugin, check if the correct privileges are provided and install the plugin.
40
+func (pm *Manager) Pull(ctx context.Context, ref reference.Named, name string, metaHeader http.Header, authConfig *registry.AuthConfig, privileges types.PluginPrivileges, out io.Writer, opts ...CreateOpt) error {
41
+	return errNotSupported
42
+}
43
+
44
+// Upgrade pulls a plugin, check if the correct privileges are provided and install the plugin.
45
+func (pm *Manager) Upgrade(ctx context.Context, ref reference.Named, name string, metaHeader http.Header, authConfig *registry.AuthConfig, privileges types.PluginPrivileges, outStream io.Writer) error {
46
+	return errNotSupported
47
+}
48
+
49
+// List displays the list of plugins and associated metadata.
50
+func (pm *Manager) List(pluginFilters filters.Args) ([]types.Plugin, error) {
51
+	return nil, errNotSupported
52
+}
53
+
54
+// Push pushes a plugin to the store.
55
+func (pm *Manager) Push(ctx context.Context, name string, metaHeader http.Header, authConfig *registry.AuthConfig, out io.Writer) error {
56
+	return errNotSupported
57
+}
58
+
59
+// Remove deletes plugin's root directory.
60
+func (pm *Manager) Remove(name string, config *backend.PluginRmConfig) error {
61
+	return errNotSupported
62
+}
63
+
64
+// Set sets plugin args
65
+func (pm *Manager) Set(name string, args []string) error {
66
+	return errNotSupported
67
+}
68
+
69
+// CreateFromContext creates a plugin from the given pluginDir which contains
70
+// both the rootfs and the config.json and a repoName with optional tag.
71
+func (pm *Manager) CreateFromContext(ctx context.Context, tarCtx io.ReadCloser, options *types.PluginCreateOptions) error {
72
+	return errNotSupported
73
+}
0 74
new file mode 100644
... ...
@@ -0,0 +1,74 @@
0
+package plugin
1
+
2
+import (
3
+	"strings"
4
+	"sync"
5
+
6
+	v2 "github.com/docker/docker/daemon/pkg/plugin/v2"
7
+	"github.com/docker/docker/pkg/plugins"
8
+	"github.com/opencontainers/runtime-spec/specs-go"
9
+)
10
+
11
+// Store manages the plugin inventory in memory and on-disk
12
+type Store struct {
13
+	sync.RWMutex
14
+	plugins  map[string]*v2.Plugin
15
+	specOpts map[string][]SpecOpt
16
+	/* handlers are necessary for transition path of legacy plugins
17
+	 * to the new model. Legacy plugins use Handle() for registering an
18
+	 * activation callback.*/
19
+	handlers map[string][]func(string, *plugins.Client)
20
+}
21
+
22
+// NewStore creates a Store.
23
+func NewStore() *Store {
24
+	return &Store{
25
+		plugins:  make(map[string]*v2.Plugin),
26
+		specOpts: make(map[string][]SpecOpt),
27
+		handlers: make(map[string][]func(string, *plugins.Client)),
28
+	}
29
+}
30
+
31
+// SpecOpt is used for subsystems that need to modify the runtime spec of a plugin
32
+type SpecOpt func(*specs.Spec)
33
+
34
+// CreateOpt is used to configure specific plugin details when created
35
+type CreateOpt func(p *v2.Plugin)
36
+
37
+// WithSwarmService is a CreateOpt that flags the passed in a plugin as a plugin
38
+// managed by swarm
39
+func WithSwarmService(id string) CreateOpt {
40
+	return func(p *v2.Plugin) {
41
+		p.SwarmServiceID = id
42
+	}
43
+}
44
+
45
+// WithEnv is a CreateOpt that passes the user-provided environment variables
46
+// to the plugin container, de-duplicating variables with the same names case
47
+// sensitively and only appends valid key=value pairs
48
+func WithEnv(env []string) CreateOpt {
49
+	return func(p *v2.Plugin) {
50
+		effectiveEnv := make(map[string]string)
51
+		for _, penv := range p.PluginObj.Config.Env {
52
+			if penv.Value != nil {
53
+				effectiveEnv[penv.Name] = *penv.Value
54
+			}
55
+		}
56
+		for _, line := range env {
57
+			if k, v, ok := strings.Cut(line, "="); ok {
58
+				effectiveEnv[k] = v
59
+			}
60
+		}
61
+		p.PluginObj.Settings.Env = make([]string, 0, len(effectiveEnv))
62
+		for key, value := range effectiveEnv {
63
+			p.PluginObj.Settings.Env = append(p.PluginObj.Settings.Env, key+"="+value)
64
+		}
65
+	}
66
+}
67
+
68
+// WithSpecMounts is a SpecOpt which appends the provided mounts to the runtime spec
69
+func WithSpecMounts(mounts []specs.Mount) SpecOpt {
70
+	return func(s *specs.Spec) {
71
+		s.Mounts = append(s.Mounts, mounts...)
72
+	}
73
+}
0 74
new file mode 100644
... ...
@@ -0,0 +1,51 @@
0
+package plugin
1
+
2
+import "fmt"
3
+
4
+type errNotFound string
5
+
6
+func (name errNotFound) Error() string {
7
+	return fmt.Sprintf("plugin %q not found", string(name))
8
+}
9
+
10
+func (errNotFound) NotFound() {}
11
+
12
+type errAmbiguous string
13
+
14
+func (name errAmbiguous) Error() string {
15
+	return fmt.Sprintf("multiple plugins found for %q", string(name))
16
+}
17
+
18
+func (name errAmbiguous) InvalidParameter() {}
19
+
20
+type errDisabled string
21
+
22
+func (name errDisabled) Error() string {
23
+	return fmt.Sprintf("plugin %s found but disabled", string(name))
24
+}
25
+
26
+func (name errDisabled) Conflict() {}
27
+
28
+type inUseError string
29
+
30
+func (e inUseError) Error() string {
31
+	return "plugin " + string(e) + " is in use"
32
+}
33
+
34
+func (inUseError) Conflict() {}
35
+
36
+type enabledError string
37
+
38
+func (e enabledError) Error() string {
39
+	return "plugin " + string(e) + " is enabled"
40
+}
41
+
42
+func (enabledError) Conflict() {}
43
+
44
+type alreadyExistsError string
45
+
46
+func (e alreadyExistsError) Error() string {
47
+	return "plugin " + string(e) + " already exists"
48
+}
49
+
50
+func (alreadyExistsError) Conflict() {}
0 51
new file mode 100644
... ...
@@ -0,0 +1,111 @@
0
+package plugin
1
+
2
+import (
3
+	"fmt"
4
+	"reflect"
5
+
6
+	"github.com/docker/docker/api/types"
7
+)
8
+
9
+// Event is emitted for actions performed on the plugin manager
10
+type Event interface {
11
+	matches(Event) bool
12
+}
13
+
14
+// EventCreate is an event which is emitted when a plugin is created
15
+// This is either by pull or create from context.
16
+//
17
+// Use the `Interfaces` field to match only plugins that implement a specific
18
+// interface.
19
+// These are matched against using "or" logic.
20
+// If no interfaces are listed, all are matched.
21
+type EventCreate struct {
22
+	Interfaces map[string]bool
23
+	Plugin     types.Plugin
24
+}
25
+
26
+func (e EventCreate) matches(observed Event) bool {
27
+	oe, ok := observed.(EventCreate)
28
+	if !ok {
29
+		return false
30
+	}
31
+	if len(e.Interfaces) == 0 {
32
+		return true
33
+	}
34
+
35
+	var ifaceMatch bool
36
+	for _, in := range oe.Plugin.Config.Interface.Types {
37
+		if e.Interfaces[in.Capability] {
38
+			ifaceMatch = true
39
+			break
40
+		}
41
+	}
42
+	return ifaceMatch
43
+}
44
+
45
+// EventRemove is an event which is emitted when a plugin is removed
46
+// It matches on the passed in plugin's ID only.
47
+type EventRemove struct {
48
+	Plugin types.Plugin
49
+}
50
+
51
+func (e EventRemove) matches(observed Event) bool {
52
+	oe, ok := observed.(EventRemove)
53
+	if !ok {
54
+		return false
55
+	}
56
+	return e.Plugin.ID == oe.Plugin.ID
57
+}
58
+
59
+// EventDisable is an event that is emitted when a plugin is disabled
60
+// It matches on the passed in plugin's ID only.
61
+type EventDisable struct {
62
+	Plugin types.Plugin
63
+}
64
+
65
+func (e EventDisable) matches(observed Event) bool {
66
+	oe, ok := observed.(EventDisable)
67
+	if !ok {
68
+		return false
69
+	}
70
+	return e.Plugin.ID == oe.Plugin.ID
71
+}
72
+
73
+// EventEnable is an event that is emitted when a plugin is disabled
74
+// It matches on the passed in plugin's ID only.
75
+type EventEnable struct {
76
+	Plugin types.Plugin
77
+}
78
+
79
+func (e EventEnable) matches(observed Event) bool {
80
+	oe, ok := observed.(EventEnable)
81
+	if !ok {
82
+		return false
83
+	}
84
+	return e.Plugin.ID == oe.Plugin.ID
85
+}
86
+
87
+// SubscribeEvents provides an event channel to listen for structured events from
88
+// the plugin manager actions, CRUD operations.
89
+// The caller must call the returned `cancel()` function once done with the channel
90
+// or this will leak resources.
91
+func (pm *Manager) SubscribeEvents(buffer int, watchEvents ...Event) (eventCh <-chan interface{}, cancel func()) {
92
+	topic := func(i interface{}) bool {
93
+		observed, ok := i.(Event)
94
+		if !ok {
95
+			panic(fmt.Sprintf("unexpected type passed to event channel: %v", reflect.TypeOf(i)))
96
+		}
97
+		for _, e := range watchEvents {
98
+			if e.matches(observed) {
99
+				return true
100
+			}
101
+		}
102
+		// If no specific events are specified always assume a matched event
103
+		// If some events were specified and none matched above, then the event
104
+		// doesn't match
105
+		return watchEvents == nil
106
+	}
107
+	ch := pm.publisher.SubscribeTopicWithBuffer(topic, buffer)
108
+	cancelFunc := func() { pm.publisher.Evict(ch) }
109
+	return ch, cancelFunc
110
+}
0 111
new file mode 100644
... ...
@@ -0,0 +1,293 @@
0
+package plugin
1
+
2
+import (
3
+	"context"
4
+	"io"
5
+	"net/http"
6
+	"time"
7
+
8
+	"github.com/containerd/containerd/v2/core/content"
9
+	c8dimages "github.com/containerd/containerd/v2/core/images"
10
+	"github.com/containerd/containerd/v2/core/remotes"
11
+	"github.com/containerd/containerd/v2/core/remotes/docker"
12
+	cerrdefs "github.com/containerd/errdefs"
13
+	"github.com/containerd/log"
14
+	"github.com/distribution/reference"
15
+	"github.com/docker/docker/api/types/registry"
16
+	progressutils "github.com/docker/docker/distribution/utils"
17
+	"github.com/docker/docker/pkg/ioutils"
18
+	"github.com/docker/docker/pkg/progress"
19
+	"github.com/docker/docker/pkg/stringid"
20
+	"github.com/moby/go-archive/chrootarchive"
21
+	"github.com/opencontainers/go-digest"
22
+	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
23
+	"github.com/pkg/errors"
24
+)
25
+
26
+const mediaTypePluginConfig = "application/vnd.docker.plugin.v1+json"
27
+
28
+// setupProgressOutput sets up the passed in writer to stream progress.
29
+//
30
+// The passed in cancel function is used by the progress writer to signal callers that there
31
+// is an issue writing to the stream.
32
+//
33
+// The returned function is used to wait for the progress writer to be finished.
34
+// Call it to make sure the progress writer is done before returning from your function as needed.
35
+func setupProgressOutput(outStream io.Writer, cancel func()) (progress.Output, func()) {
36
+	var out progress.Output
37
+	f := func() {}
38
+
39
+	if outStream != nil {
40
+		ch := make(chan progress.Progress, 100)
41
+		out = progress.ChanOutput(ch)
42
+
43
+		ctx, retCancel := context.WithCancel(context.Background())
44
+		go func() {
45
+			progressutils.WriteDistributionProgress(cancel, outStream, ch)
46
+			retCancel()
47
+		}()
48
+
49
+		f = func() {
50
+			close(ch)
51
+			<-ctx.Done()
52
+		}
53
+	} else {
54
+		out = progress.DiscardOutput()
55
+	}
56
+	return out, f
57
+}
58
+
59
+// fetch the content related to the passed in reference into the blob store and appends the provided c8dimages.Handlers
60
+// There is no need to use remotes.FetchHandler since it already gets set
61
+func (pm *Manager) fetch(ctx context.Context, ref reference.Named, auth *registry.AuthConfig, out progress.Output, metaHeader http.Header, handlers ...c8dimages.Handler) error {
62
+	// We need to make sure we have a domain on the reference
63
+	withDomain, err := reference.ParseNormalizedNamed(ref.String())
64
+	if err != nil {
65
+		return errors.Wrap(err, "error parsing plugin image reference")
66
+	}
67
+
68
+	// Make sure we can authenticate the request since the auth scope for plugin repos is different than a normal repo.
69
+	ctx = docker.WithScope(ctx, scope(ref, false))
70
+
71
+	// Make sure the fetch handler knows how to set a ref key for the plugin media type.
72
+	// Without this the ref key is "unknown" and we see a nasty warning message in the logs
73
+	ctx = remotes.WithMediaTypeKeyPrefix(ctx, mediaTypePluginConfig, "docker-plugin")
74
+
75
+	resolver, err := pm.newResolver(ctx, nil, auth, metaHeader, false)
76
+	if err != nil {
77
+		return err
78
+	}
79
+	resolved, desc, err := resolver.Resolve(ctx, withDomain.String())
80
+	if err != nil {
81
+		// This is backwards compatible with older versions of the distribution registry.
82
+		// The containerd client will add it's own accept header as a comma separated list of supported manifests.
83
+		// This is perfectly fine, unless you are talking to an older registry which does not split the comma separated list,
84
+		//   so it is never able to match a media type and it falls back to schema1 (yuck) and fails because our manifest the
85
+		//   fallback does not support plugin configs...
86
+		log.G(ctx).WithError(err).WithField("ref", withDomain).Debug("Error while resolving reference, falling back to backwards compatible accept header format")
87
+		headers := http.Header{}
88
+		headers.Add("Accept", c8dimages.MediaTypeDockerSchema2Manifest)
89
+		headers.Add("Accept", c8dimages.MediaTypeDockerSchema2ManifestList)
90
+		headers.Add("Accept", ocispec.MediaTypeImageManifest)
91
+		headers.Add("Accept", ocispec.MediaTypeImageIndex)
92
+		resolver, _ = pm.newResolver(ctx, nil, auth, headers, false)
93
+		if resolver != nil {
94
+			resolved, desc, err = resolver.Resolve(ctx, withDomain.String())
95
+			if err != nil {
96
+				log.G(ctx).WithError(err).WithField("ref", withDomain).Debug("Failed to resolve reference after falling back to backwards compatible accept header format")
97
+			}
98
+		}
99
+		if err != nil {
100
+			return errors.Wrap(err, "error resolving plugin reference")
101
+		}
102
+	}
103
+
104
+	fetcher, err := resolver.Fetcher(ctx, resolved)
105
+	if err != nil {
106
+		return errors.Wrap(err, "error creating plugin image fetcher")
107
+	}
108
+
109
+	fp := withFetchProgress(pm.blobStore, out, ref)
110
+	handlers = append([]c8dimages.Handler{fp, remotes.FetchHandler(pm.blobStore, fetcher)}, handlers...)
111
+	return c8dimages.Dispatch(ctx, c8dimages.Handlers(handlers...), nil, desc)
112
+}
113
+
114
+// applyLayer makes an c8dimages.HandlerFunc which applies a fetched image rootfs layer to a directory.
115
+//
116
+// TODO(@cpuguy83) This gets run sequentially after layer pull (makes sense), however
117
+// if there are multiple layers to fetch we may end up extracting layers in the wrong
118
+// order.
119
+func applyLayer(cs content.Store, dir string, out progress.Output) c8dimages.HandlerFunc {
120
+	return func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
121
+		switch desc.MediaType {
122
+		case
123
+			ocispec.MediaTypeImageLayer,
124
+			c8dimages.MediaTypeDockerSchema2Layer,
125
+			ocispec.MediaTypeImageLayerGzip,
126
+			c8dimages.MediaTypeDockerSchema2LayerGzip:
127
+		default:
128
+			return nil, nil
129
+		}
130
+
131
+		ra, err := cs.ReaderAt(ctx, desc)
132
+		if err != nil {
133
+			return nil, errors.Wrapf(err, "error getting content from content store for digest %s", desc.Digest)
134
+		}
135
+
136
+		id := stringid.TruncateID(desc.Digest.String())
137
+
138
+		rc := ioutils.NewReadCloserWrapper(content.NewReader(ra), ra.Close)
139
+		pr := progress.NewProgressReader(rc, out, desc.Size, id, "Extracting")
140
+		defer pr.Close()
141
+
142
+		if _, err := chrootarchive.ApplyLayer(dir, pr); err != nil {
143
+			return nil, errors.Wrapf(err, "error applying layer for digest %s", desc.Digest)
144
+		}
145
+		progress.Update(out, id, "Complete")
146
+		return nil, nil
147
+	}
148
+}
149
+
150
+func childrenHandler(cs content.Store) c8dimages.HandlerFunc {
151
+	ch := c8dimages.ChildrenHandler(cs)
152
+	return func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
153
+		switch desc.MediaType {
154
+		case mediaTypePluginConfig:
155
+			return nil, nil
156
+		default:
157
+			return ch(ctx, desc)
158
+		}
159
+	}
160
+}
161
+
162
+type fetchMeta struct {
163
+	blobs    []digest.Digest
164
+	config   digest.Digest
165
+	manifest digest.Digest
166
+}
167
+
168
+func storeFetchMetadata(m *fetchMeta) c8dimages.HandlerFunc {
169
+	return func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
170
+		switch desc.MediaType {
171
+		case
172
+			c8dimages.MediaTypeDockerSchema2LayerForeignGzip,
173
+			c8dimages.MediaTypeDockerSchema2Layer,
174
+			ocispec.MediaTypeImageLayer,
175
+			ocispec.MediaTypeImageLayerGzip:
176
+			m.blobs = append(m.blobs, desc.Digest)
177
+		case ocispec.MediaTypeImageManifest, c8dimages.MediaTypeDockerSchema2Manifest:
178
+			m.manifest = desc.Digest
179
+		case mediaTypePluginConfig:
180
+			m.config = desc.Digest
181
+		}
182
+		return nil, nil
183
+	}
184
+}
185
+
186
+func validateFetchedMetadata(md fetchMeta) error {
187
+	if md.config == "" {
188
+		return errors.New("fetched plugin image but plugin config is missing")
189
+	}
190
+	if md.manifest == "" {
191
+		return errors.New("fetched plugin image but manifest is missing")
192
+	}
193
+	return nil
194
+}
195
+
196
+// withFetchProgress is a fetch handler which registers a descriptor with a progress
197
+func withFetchProgress(cs content.Store, out progress.Output, ref reference.Named) c8dimages.HandlerFunc {
198
+	return func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
199
+		switch desc.MediaType {
200
+		case ocispec.MediaTypeImageManifest, c8dimages.MediaTypeDockerSchema2Manifest:
201
+			tn := reference.TagNameOnly(ref)
202
+			var tagOrDigest string
203
+			if tagged, ok := tn.(reference.Tagged); ok {
204
+				tagOrDigest = tagged.Tag()
205
+			} else {
206
+				tagOrDigest = tn.String()
207
+			}
208
+			progress.Messagef(out, tagOrDigest, "Pulling from %s", reference.FamiliarName(ref))
209
+			progress.Messagef(out, "", "Digest: %s", desc.Digest.String())
210
+			return nil, nil
211
+		case
212
+			c8dimages.MediaTypeDockerSchema2LayerGzip,
213
+			c8dimages.MediaTypeDockerSchema2Layer,
214
+			ocispec.MediaTypeImageLayer,
215
+			ocispec.MediaTypeImageLayerGzip:
216
+		default:
217
+			return nil, nil
218
+		}
219
+
220
+		id := stringid.TruncateID(desc.Digest.String())
221
+
222
+		if _, err := cs.Info(ctx, desc.Digest); err == nil {
223
+			out.WriteProgress(progress.Progress{ID: id, Action: "Already exists", LastUpdate: true})
224
+			return nil, nil
225
+		}
226
+
227
+		progress.Update(out, id, "Waiting")
228
+
229
+		key := remotes.MakeRefKey(ctx, desc)
230
+
231
+		go func() {
232
+			timer := time.NewTimer(100 * time.Millisecond)
233
+			if !timer.Stop() {
234
+				<-timer.C
235
+			}
236
+			defer timer.Stop()
237
+
238
+			var pulling bool
239
+			var (
240
+				// make sure we can still fetch from the content store
241
+				// if the main context is cancelled
242
+				// TODO: Might need to add some sort of timeout; see https://github.com/moby/moby/issues/49413
243
+				ctxErr      error
244
+				noCancelCTX = context.WithoutCancel(ctx)
245
+			)
246
+
247
+			for {
248
+				timer.Reset(100 * time.Millisecond)
249
+
250
+				select {
251
+				case <-ctx.Done():
252
+					ctxErr = ctx.Err()
253
+				case <-timer.C:
254
+				}
255
+
256
+				s, err := cs.Status(noCancelCTX, key)
257
+				if err != nil {
258
+					if !cerrdefs.IsNotFound(err) {
259
+						log.G(noCancelCTX).WithError(err).WithField("layerDigest", desc.Digest.String()).Error("Error looking up status of plugin layer pull")
260
+						progress.Update(out, id, err.Error())
261
+						return
262
+					}
263
+
264
+					if _, err := cs.Info(noCancelCTX, desc.Digest); err == nil {
265
+						progress.Update(out, id, "Download complete")
266
+						return
267
+					}
268
+
269
+					if ctxErr != nil {
270
+						progress.Update(out, id, ctxErr.Error())
271
+						return
272
+					}
273
+
274
+					continue
275
+				}
276
+
277
+				if !pulling {
278
+					progress.Update(out, id, "Pulling fs layer")
279
+					pulling = true
280
+				}
281
+
282
+				if s.Offset == s.Total {
283
+					out.WriteProgress(progress.Progress{ID: id, Action: "Download complete", Current: s.Offset, LastUpdate: true})
284
+					return
285
+				}
286
+
287
+				out.WriteProgress(progress.Progress{ID: id, Action: "Downloading", Current: s.Offset, Total: s.Total})
288
+			}
289
+		}()
290
+		return nil, nil
291
+	}
292
+}
0 293
new file mode 100644
... ...
@@ -0,0 +1,370 @@
0
+package plugin
1
+
2
+import (
3
+	"context"
4
+	"encoding/json"
5
+	"io"
6
+	"os"
7
+	"path/filepath"
8
+	"reflect"
9
+	"sort"
10
+	"strings"
11
+	"sync"
12
+	"syscall"
13
+
14
+	"github.com/containerd/containerd/v2/core/content"
15
+	"github.com/containerd/containerd/v2/plugins/content/local"
16
+	"github.com/containerd/log"
17
+	"github.com/docker/docker/api/types"
18
+	"github.com/docker/docker/api/types/events"
19
+	v2 "github.com/docker/docker/daemon/pkg/plugin/v2"
20
+	"github.com/docker/docker/internal/containerfs"
21
+	"github.com/docker/docker/internal/lazyregexp"
22
+	"github.com/docker/docker/pkg/authorization"
23
+	"github.com/docker/docker/registry"
24
+	"github.com/moby/pubsub"
25
+	"github.com/moby/sys/atomicwriter"
26
+	"github.com/opencontainers/go-digest"
27
+	"github.com/opencontainers/runtime-spec/specs-go"
28
+	"github.com/pkg/errors"
29
+	"github.com/sirupsen/logrus"
30
+)
31
+
32
+const (
33
+	configFileName = "config.json"
34
+	rootFSFileName = "rootfs"
35
+)
36
+
37
+var validFullID = lazyregexp.New(`^([a-f0-9]{64})$`)
38
+
39
+// Executor is the interface that the plugin manager uses to interact with for starting/stopping plugins
40
+type Executor interface {
41
+	Create(id string, spec specs.Spec, stdout, stderr io.WriteCloser) error
42
+	IsRunning(id string) (bool, error)
43
+	Restore(id string, stdout, stderr io.WriteCloser) (alive bool, err error)
44
+	Signal(id string, signal syscall.Signal) error
45
+}
46
+
47
+// EndpointResolver provides looking up registry endpoints for pulling.
48
+type EndpointResolver interface {
49
+	LookupPullEndpoints(hostname string) (endpoints []registry.APIEndpoint, err error)
50
+}
51
+
52
+func (pm *Manager) restorePlugin(p *v2.Plugin, c *controller) error {
53
+	if p.IsEnabled() {
54
+		return pm.restore(p, c)
55
+	}
56
+	return nil
57
+}
58
+
59
+type eventLogger func(id, name string, action events.Action)
60
+
61
+// ManagerConfig defines configuration needed to start new manager.
62
+type ManagerConfig struct {
63
+	Store              *Store // remove
64
+	RegistryService    EndpointResolver
65
+	LiveRestoreEnabled bool // TODO: remove
66
+	LogPluginEvent     eventLogger
67
+	Root               string
68
+	ExecRoot           string
69
+	CreateExecutor     ExecutorCreator
70
+	AuthzMiddleware    *authorization.Middleware
71
+}
72
+
73
+// ExecutorCreator is used in the manager config to pass in an `Executor`
74
+type ExecutorCreator func(*Manager) (Executor, error)
75
+
76
+// Manager controls the plugin subsystem.
77
+type Manager struct {
78
+	config    ManagerConfig
79
+	mu        sync.RWMutex // protects cMap
80
+	muGC      sync.RWMutex // protects blobstore deletions
81
+	cMap      map[*v2.Plugin]*controller
82
+	blobStore content.Store
83
+	publisher *pubsub.Publisher
84
+	executor  Executor
85
+}
86
+
87
+// controller represents the manager's control on a plugin.
88
+type controller struct {
89
+	restart       bool
90
+	exitChan      chan bool
91
+	timeoutInSecs int
92
+}
93
+
94
+// NewManager returns a new plugin manager.
95
+func NewManager(config ManagerConfig) (*Manager, error) {
96
+	manager := &Manager{
97
+		config: config,
98
+	}
99
+	for _, dirName := range []string{manager.config.Root, manager.config.ExecRoot, manager.tmpDir()} {
100
+		if err := os.MkdirAll(dirName, 0o700); err != nil {
101
+			return nil, errors.Wrapf(err, "failed to mkdir %v", dirName)
102
+		}
103
+	}
104
+	var err error
105
+	manager.executor, err = config.CreateExecutor(manager)
106
+	if err != nil {
107
+		return nil, err
108
+	}
109
+
110
+	manager.blobStore, err = local.NewStore(filepath.Join(manager.config.Root, "storage"))
111
+	if err != nil {
112
+		return nil, errors.Wrap(err, "error creating plugin blob store")
113
+	}
114
+
115
+	manager.cMap = make(map[*v2.Plugin]*controller)
116
+	if err := manager.reload(); err != nil {
117
+		return nil, errors.Wrap(err, "failed to restore plugins")
118
+	}
119
+
120
+	manager.publisher = pubsub.NewPublisher(0, 0)
121
+	return manager, nil
122
+}
123
+
124
+func (pm *Manager) tmpDir() string {
125
+	return filepath.Join(pm.config.Root, "tmp")
126
+}
127
+
128
+// HandleExitEvent is called when the executor receives the exit event
129
+// In the future we may change this, but for now all we care about is the exit event.
130
+func (pm *Manager) HandleExitEvent(id string) error {
131
+	p, err := pm.config.Store.GetV2Plugin(id)
132
+	if err != nil {
133
+		return err
134
+	}
135
+
136
+	if err := os.RemoveAll(filepath.Join(pm.config.ExecRoot, id)); err != nil {
137
+		log.G(context.TODO()).WithError(err).WithField("id", id).Error("Could not remove plugin bundle dir")
138
+	}
139
+
140
+	pm.mu.RLock()
141
+	c := pm.cMap[p]
142
+	if c.exitChan != nil {
143
+		close(c.exitChan)
144
+		c.exitChan = nil // ignore duplicate events (containerd issue #2299)
145
+	}
146
+	restart := c.restart
147
+	pm.mu.RUnlock()
148
+
149
+	if restart {
150
+		pm.enable(p, c, true)
151
+	} else if err := recursiveUnmount(filepath.Join(pm.config.Root, id)); err != nil {
152
+		return errors.Wrap(err, "error cleaning up plugin mounts")
153
+	}
154
+	return nil
155
+}
156
+
157
+func handleLoadError(err error, id string) {
158
+	if err == nil {
159
+		return
160
+	}
161
+	logger := log.G(context.TODO()).WithError(err).WithField("id", id)
162
+	if errors.Is(err, os.ErrNotExist) {
163
+		// Likely some error while removing on an older version of docker
164
+		logger.Warn("missing plugin config, skipping: this may be caused due to a failed remove and requires manual cleanup.")
165
+		return
166
+	}
167
+	logger.Error("error loading plugin, skipping")
168
+}
169
+
170
+func (pm *Manager) reload() error { // todo: restore
171
+	dir, err := os.ReadDir(pm.config.Root)
172
+	if err != nil {
173
+		return errors.Wrapf(err, "failed to read %v", pm.config.Root)
174
+	}
175
+	plugins := make(map[string]*v2.Plugin)
176
+	for _, v := range dir {
177
+		if validFullID.MatchString(v.Name()) {
178
+			p, err := pm.loadPlugin(v.Name())
179
+			if err != nil {
180
+				handleLoadError(err, v.Name())
181
+				continue
182
+			}
183
+			plugins[p.GetID()] = p
184
+		} else {
185
+			if validFullID.MatchString(strings.TrimSuffix(v.Name(), "-removing")) {
186
+				// There was likely some error while removing this plugin, let's try to remove again here
187
+				if err := containerfs.EnsureRemoveAll(v.Name()); err != nil {
188
+					log.G(context.TODO()).WithError(err).WithField("id", v.Name()).Warn("error while attempting to clean up previously removed plugin")
189
+				}
190
+			}
191
+		}
192
+	}
193
+
194
+	pm.config.Store.SetAll(plugins)
195
+
196
+	var wg sync.WaitGroup
197
+	wg.Add(len(plugins))
198
+	for _, p := range plugins {
199
+		c := &controller{exitChan: make(chan bool)}
200
+		pm.mu.Lock()
201
+		pm.cMap[p] = c
202
+		pm.mu.Unlock()
203
+
204
+		go func(p *v2.Plugin) {
205
+			defer wg.Done()
206
+			// TODO(thaJeztah): make this fail if the plugin has "graphdriver" capability ?
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
+						if typ.Capability == "graphdriver" {
223
+							// TODO(thaJeztah): remove this for next release.
224
+							log.G(context.TODO()).WithError(err).WithField("dir", propRoot).Warn("skipping migrating propagated mount storage for deprecated graphdriver plugin")
225
+						}
226
+
227
+						// check if we need to migrate an older propagated mount from before
228
+						// these mounts were stored outside the plugin rootfs
229
+						if _, err := os.Stat(propRoot); os.IsNotExist(err) {
230
+							rootfsProp := filepath.Join(p.Rootfs, p.PluginObj.Config.PropagatedMount)
231
+							if _, err := os.Stat(rootfsProp); err == nil {
232
+								if err := os.Rename(rootfsProp, propRoot); err != nil {
233
+									log.G(context.TODO()).WithError(err).WithField("dir", propRoot).Error("error migrating propagated mount storage")
234
+								}
235
+							}
236
+						}
237
+
238
+						if err := os.MkdirAll(propRoot, 0o755); err != nil {
239
+							log.G(context.TODO()).Errorf("failed to create PropagatedMount directory at %s: %v", propRoot, err)
240
+						}
241
+					}
242
+				}
243
+			}
244
+
245
+			pm.save(p)
246
+			requiresManualRestore := !pm.config.LiveRestoreEnabled && p.IsEnabled()
247
+
248
+			if requiresManualRestore {
249
+				// if liveRestore is not enabled, the plugin will be stopped now so we should enable it
250
+				if err := pm.enable(p, c, true); err != nil {
251
+					log.G(context.TODO()).WithError(err).WithField("id", p.GetID()).Error("failed to enable plugin")
252
+				}
253
+			}
254
+		}(p)
255
+	}
256
+	wg.Wait()
257
+	return nil
258
+}
259
+
260
+// Get looks up the requested plugin in the store.
261
+func (pm *Manager) Get(idOrName string) (*v2.Plugin, error) {
262
+	return pm.config.Store.GetV2Plugin(idOrName)
263
+}
264
+
265
+func (pm *Manager) loadPlugin(id string) (*v2.Plugin, error) {
266
+	p := filepath.Join(pm.config.Root, id, configFileName)
267
+	dt, err := os.ReadFile(p)
268
+	if err != nil {
269
+		return nil, errors.Wrapf(err, "error reading %v", p)
270
+	}
271
+	var plugin v2.Plugin
272
+	if err := json.Unmarshal(dt, &plugin); err != nil {
273
+		return nil, errors.Wrapf(err, "error decoding %v", p)
274
+	}
275
+	return &plugin, nil
276
+}
277
+
278
+func (pm *Manager) save(p *v2.Plugin) error {
279
+	pluginJSON, err := json.Marshal(p)
280
+	if err != nil {
281
+		return errors.Wrap(err, "failed to marshal plugin json")
282
+	}
283
+	if err := atomicwriter.WriteFile(filepath.Join(pm.config.Root, p.GetID(), configFileName), pluginJSON, 0o600); err != nil {
284
+		return errors.Wrap(err, "failed to write atomically plugin json")
285
+	}
286
+	return nil
287
+}
288
+
289
+// GC cleans up unreferenced blobs. This is recommended to run in a goroutine
290
+func (pm *Manager) GC() {
291
+	pm.muGC.Lock()
292
+	defer pm.muGC.Unlock()
293
+
294
+	used := make(map[digest.Digest]struct{})
295
+	for _, p := range pm.config.Store.GetAll() {
296
+		used[p.Config] = struct{}{}
297
+		for _, b := range p.Blobsums {
298
+			used[b] = struct{}{}
299
+		}
300
+	}
301
+
302
+	ctx := context.TODO()
303
+	pm.blobStore.Walk(ctx, func(info content.Info) error {
304
+		_, ok := used[info.Digest]
305
+		if ok {
306
+			return nil
307
+		}
308
+
309
+		return pm.blobStore.Delete(ctx, info.Digest)
310
+	})
311
+}
312
+
313
+type logHook struct{ id string }
314
+
315
+func (logHook) Levels() []log.Level {
316
+	return []log.Level{
317
+		log.PanicLevel,
318
+		log.FatalLevel,
319
+		log.ErrorLevel,
320
+		log.WarnLevel,
321
+		log.InfoLevel,
322
+		log.DebugLevel,
323
+		log.TraceLevel,
324
+	}
325
+}
326
+
327
+func (l logHook) Fire(entry *log.Entry) error {
328
+	entry.Data = log.Fields{"plugin": l.id}
329
+	return nil
330
+}
331
+
332
+func makeLoggerStreams(id string) (stdout, stderr io.WriteCloser) {
333
+	logger := logrus.New()
334
+	logger.Hooks.Add(logHook{id})
335
+	return logger.WriterLevel(log.InfoLevel), logger.WriterLevel(log.ErrorLevel)
336
+}
337
+
338
+func validatePrivileges(requiredPrivileges, privileges types.PluginPrivileges) error {
339
+	if !isEqual(requiredPrivileges, privileges, isEqualPrivilege) {
340
+		return errors.New("incorrect privileges")
341
+	}
342
+
343
+	return nil
344
+}
345
+
346
+func isEqual(arrOne, arrOther types.PluginPrivileges, compare func(x, y types.PluginPrivilege) bool) bool {
347
+	if len(arrOne) != len(arrOther) {
348
+		return false
349
+	}
350
+
351
+	sort.Sort(arrOne)
352
+	sort.Sort(arrOther)
353
+
354
+	for i := 1; i < arrOne.Len(); i++ {
355
+		if !compare(arrOne[i], arrOther[i]) {
356
+			return false
357
+		}
358
+	}
359
+
360
+	return true
361
+}
362
+
363
+func isEqualPrivilege(a, b types.PluginPrivilege) bool {
364
+	if a.Name != b.Name {
365
+		return false
366
+	}
367
+
368
+	return reflect.DeepEqual(a.Value, b.Value)
369
+}
0 370
new file mode 100644
... ...
@@ -0,0 +1,351 @@
0
+package plugin
1
+
2
+import (
3
+	"context"
4
+	"encoding/json"
5
+	"net"
6
+	"os"
7
+	"path/filepath"
8
+	"time"
9
+
10
+	"github.com/containerd/containerd/v2/core/content"
11
+	"github.com/containerd/log"
12
+	"github.com/docker/docker/api/types"
13
+	"github.com/docker/docker/daemon/initlayer"
14
+	v2 "github.com/docker/docker/daemon/pkg/plugin/v2"
15
+	"github.com/docker/docker/errdefs"
16
+	"github.com/docker/docker/pkg/plugins"
17
+	"github.com/docker/docker/pkg/stringid"
18
+	"github.com/moby/sys/mount"
19
+	"github.com/opencontainers/go-digest"
20
+	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
21
+	"github.com/pkg/errors"
22
+	"golang.org/x/sys/unix"
23
+)
24
+
25
+func (pm *Manager) enable(p *v2.Plugin, c *controller, force bool) error {
26
+	p.Rootfs = filepath.Join(pm.config.Root, p.PluginObj.ID, "rootfs")
27
+	if p.IsEnabled() && !force {
28
+		return errors.Wrap(enabledError(p.Name()), "plugin already enabled")
29
+	}
30
+	spec, err := p.InitSpec(pm.config.ExecRoot)
31
+	if err != nil {
32
+		return err
33
+	}
34
+
35
+	c.restart = true
36
+	c.exitChan = make(chan bool)
37
+
38
+	pm.mu.Lock()
39
+	pm.cMap[p] = c
40
+	pm.mu.Unlock()
41
+
42
+	var propRoot string
43
+	if p.PluginObj.Config.PropagatedMount != "" {
44
+		propRoot = filepath.Join(filepath.Dir(p.Rootfs), "propagated-mount")
45
+
46
+		if err := os.MkdirAll(propRoot, 0o755); err != nil {
47
+			log.G(context.TODO()).Errorf("failed to create PropagatedMount directory at %s: %v", propRoot, err)
48
+		}
49
+
50
+		if err := mount.MakeRShared(propRoot); err != nil {
51
+			return errors.Wrap(err, "error setting up propagated mount dir")
52
+		}
53
+	}
54
+
55
+	rootFS := filepath.Join(pm.config.Root, p.PluginObj.ID, rootFSFileName)
56
+	if err := initlayer.Setup(rootFS, 0, 0); err != nil {
57
+		return errors.WithStack(err)
58
+	}
59
+
60
+	stdout, stderr := makeLoggerStreams(p.GetID())
61
+	if err := pm.executor.Create(p.GetID(), *spec, stdout, stderr); err != nil {
62
+		if p.PluginObj.Config.PropagatedMount != "" {
63
+			if err := mount.Unmount(propRoot); err != nil {
64
+				log.G(context.TODO()).WithField("plugin", p.Name()).WithError(err).Warn("Failed to unmount vplugin propagated mount root")
65
+			}
66
+		}
67
+		return errors.WithStack(err)
68
+	}
69
+	return pm.pluginPostStart(p, c)
70
+}
71
+
72
+func (pm *Manager) pluginPostStart(p *v2.Plugin, c *controller) error {
73
+	sockAddr := filepath.Join(pm.config.ExecRoot, p.GetID(), p.GetSocket())
74
+	p.SetTimeout(time.Duration(c.timeoutInSecs) * time.Second)
75
+	addr := &net.UnixAddr{Net: "unix", Name: sockAddr}
76
+	p.SetAddr(addr)
77
+
78
+	if p.Protocol() == plugins.ProtocolSchemeHTTPV1 {
79
+		client, err := plugins.NewClientWithTimeout(addr.Network()+"://"+addr.String(), nil, p.Timeout())
80
+		if err != nil {
81
+			c.restart = false
82
+			shutdownPlugin(p, c.exitChan, pm.executor)
83
+			return errors.WithStack(err)
84
+		}
85
+
86
+		p.SetPClient(client) //nolint:staticcheck // FIXME(thaJeztah): p.SetPClient is deprecated: Hardcoded plugin client is deprecated
87
+	}
88
+
89
+	// Initial sleep before net Dial to allow plugin to listen on socket.
90
+	time.Sleep(500 * time.Millisecond)
91
+	maxRetries := 3
92
+	var retries int
93
+	for {
94
+		// net dial into the unix socket to see if someone's listening.
95
+		conn, err := net.Dial("unix", sockAddr)
96
+		if err == nil {
97
+			conn.Close()
98
+			break
99
+		}
100
+
101
+		time.Sleep(3 * time.Second)
102
+		retries++
103
+
104
+		if retries > maxRetries {
105
+			log.G(context.TODO()).Debugf("error net dialing plugin: %v", err)
106
+			c.restart = false
107
+			// While restoring plugins, we need to explicitly set the state to disabled
108
+			pm.config.Store.SetState(p, false)
109
+			shutdownPlugin(p, c.exitChan, pm.executor)
110
+			return err
111
+		}
112
+	}
113
+	pm.config.Store.SetState(p, true)
114
+	pm.config.Store.CallHandler(p)
115
+
116
+	return pm.save(p)
117
+}
118
+
119
+func (pm *Manager) restore(p *v2.Plugin, c *controller) error {
120
+	stdout, stderr := makeLoggerStreams(p.GetID())
121
+	alive, err := pm.executor.Restore(p.GetID(), stdout, stderr)
122
+	if err != nil {
123
+		return err
124
+	}
125
+
126
+	if pm.config.LiveRestoreEnabled {
127
+		if !alive {
128
+			return pm.enable(p, c, true)
129
+		}
130
+
131
+		c.exitChan = make(chan bool)
132
+		c.restart = true
133
+		pm.mu.Lock()
134
+		pm.cMap[p] = c
135
+		pm.mu.Unlock()
136
+		return pm.pluginPostStart(p, c)
137
+	}
138
+
139
+	if alive {
140
+		// TODO(@cpuguy83): Should we always just re-attach to the running plugin instead of doing this?
141
+		c.restart = false
142
+		shutdownPlugin(p, c.exitChan, pm.executor)
143
+	}
144
+
145
+	return nil
146
+}
147
+
148
+const shutdownTimeout = 10 * time.Second
149
+
150
+func shutdownPlugin(p *v2.Plugin, ec chan bool, executor Executor) {
151
+	pluginID := p.GetID()
152
+
153
+	if err := executor.Signal(pluginID, unix.SIGTERM); err != nil {
154
+		log.G(context.TODO()).Errorf("Sending SIGTERM to plugin failed with error: %v", err)
155
+		return
156
+	}
157
+
158
+	timeout := time.NewTimer(shutdownTimeout)
159
+	defer timeout.Stop()
160
+
161
+	select {
162
+	case <-ec:
163
+		log.G(context.TODO()).Debug("Clean shutdown of plugin")
164
+	case <-timeout.C:
165
+		log.G(context.TODO()).Debug("Force shutdown plugin")
166
+		if err := executor.Signal(pluginID, unix.SIGKILL); err != nil {
167
+			log.G(context.TODO()).Errorf("Sending SIGKILL to plugin failed with error: %v", err)
168
+		}
169
+
170
+		timeout.Reset(shutdownTimeout)
171
+
172
+		select {
173
+		case <-ec:
174
+			log.G(context.TODO()).Debug("SIGKILL plugin shutdown")
175
+		case <-timeout.C:
176
+			log.G(context.TODO()).WithField("plugin", p.Name).Warn("Force shutdown plugin FAILED")
177
+		}
178
+	}
179
+}
180
+
181
+func (pm *Manager) disable(p *v2.Plugin, c *controller) error {
182
+	if !p.IsEnabled() {
183
+		return errors.Wrap(errDisabled(p.Name()), "plugin is already disabled")
184
+	}
185
+
186
+	c.restart = false
187
+	shutdownPlugin(p, c.exitChan, pm.executor)
188
+	pm.config.Store.SetState(p, false)
189
+	return pm.save(p)
190
+}
191
+
192
+// Shutdown stops all plugins and called during daemon shutdown.
193
+func (pm *Manager) Shutdown() {
194
+	plugins := pm.config.Store.GetAll()
195
+	for _, p := range plugins {
196
+		pm.mu.RLock()
197
+		c := pm.cMap[p]
198
+		pm.mu.RUnlock()
199
+
200
+		if pm.config.LiveRestoreEnabled && p.IsEnabled() {
201
+			log.G(context.TODO()).Debug("Plugin active when liveRestore is set, skipping shutdown")
202
+			continue
203
+		}
204
+		if pm.executor != nil && p.IsEnabled() {
205
+			c.restart = false
206
+			shutdownPlugin(p, c.exitChan, pm.executor)
207
+		}
208
+	}
209
+	if err := mount.RecursiveUnmount(pm.config.Root); err != nil {
210
+		log.G(context.TODO()).WithError(err).Warn("error cleaning up plugin mounts")
211
+	}
212
+}
213
+
214
+func (pm *Manager) upgradePlugin(p *v2.Plugin, configDigest, manifestDigest digest.Digest, blobsums []digest.Digest, tmpRootFSDir string, privileges *types.PluginPrivileges) (retErr error) {
215
+	config, err := pm.setupNewPlugin(configDigest, privileges)
216
+	if err != nil {
217
+		return err
218
+	}
219
+
220
+	pdir := filepath.Join(pm.config.Root, p.PluginObj.ID)
221
+	orig := filepath.Join(pdir, "rootfs")
222
+
223
+	// Make sure nothing is mounted
224
+	// This could happen if the plugin was disabled with `-f` with active mounts.
225
+	// If there is anything in `orig` is still mounted, this should error out.
226
+	if err := mount.RecursiveUnmount(orig); err != nil {
227
+		return errdefs.System(err)
228
+	}
229
+
230
+	backup := orig + "-old"
231
+	if err := os.Rename(orig, backup); err != nil {
232
+		return errors.Wrap(errdefs.System(err), "error backing up plugin data before upgrade")
233
+	}
234
+
235
+	defer func() {
236
+		if retErr != nil {
237
+			if err := os.RemoveAll(orig); err != nil {
238
+				log.G(context.TODO()).WithError(err).WithField("dir", backup).Error("error cleaning up after failed upgrade")
239
+				return
240
+			}
241
+			if err := os.Rename(backup, orig); err != nil {
242
+				retErr = errors.Wrap(err, "error restoring old plugin root on upgrade failure")
243
+			}
244
+			if err := os.RemoveAll(tmpRootFSDir); err != nil && !os.IsNotExist(err) {
245
+				log.G(context.TODO()).WithError(err).WithField("plugin", p.Name()).Errorf("error cleaning up plugin upgrade dir: %s", tmpRootFSDir)
246
+			}
247
+		} else {
248
+			if err := os.RemoveAll(backup); err != nil {
249
+				log.G(context.TODO()).WithError(err).WithField("dir", backup).Error("error cleaning up old plugin root after successful upgrade")
250
+			}
251
+
252
+			p.Config = configDigest
253
+			p.Blobsums = blobsums
254
+		}
255
+	}()
256
+
257
+	if err := os.Rename(tmpRootFSDir, orig); err != nil {
258
+		return errors.Wrap(errdefs.System(err), "error upgrading")
259
+	}
260
+
261
+	p.PluginObj.Config = config
262
+	p.Manifest = manifestDigest
263
+	if err := pm.save(p); err != nil {
264
+		return errors.Wrap(err, "error saving upgraded plugin config")
265
+	}
266
+
267
+	return nil
268
+}
269
+
270
+func (pm *Manager) setupNewPlugin(configDigest digest.Digest, privileges *types.PluginPrivileges) (types.PluginConfig, error) {
271
+	configRA, err := pm.blobStore.ReaderAt(context.TODO(), ocispec.Descriptor{Digest: configDigest})
272
+	if err != nil {
273
+		return types.PluginConfig{}, err
274
+	}
275
+	defer configRA.Close()
276
+
277
+	configR := content.NewReader(configRA)
278
+
279
+	var config types.PluginConfig
280
+	dec := json.NewDecoder(configR)
281
+	if err := dec.Decode(&config); err != nil {
282
+		return types.PluginConfig{}, errors.Wrapf(err, "failed to parse config")
283
+	}
284
+	if dec.More() {
285
+		return types.PluginConfig{}, errors.New("invalid config json")
286
+	}
287
+
288
+	requiredPrivileges := computePrivileges(config)
289
+	if privileges != nil {
290
+		if err := validatePrivileges(requiredPrivileges, *privileges); err != nil {
291
+			return types.PluginConfig{}, err
292
+		}
293
+	}
294
+
295
+	return config, nil
296
+}
297
+
298
+// createPlugin creates a new plugin. take lock before calling.
299
+func (pm *Manager) createPlugin(name string, configDigest, manifestDigest digest.Digest, blobsums []digest.Digest, rootFSDir string, privileges *types.PluginPrivileges, opts ...CreateOpt) (_ *v2.Plugin, retErr error) {
300
+	if err := pm.config.Store.validateName(name); err != nil { // todo: this check is wrong. remove store
301
+		return nil, errdefs.InvalidParameter(err)
302
+	}
303
+
304
+	config, err := pm.setupNewPlugin(configDigest, privileges)
305
+	if err != nil {
306
+		return nil, err
307
+	}
308
+
309
+	p := &v2.Plugin{
310
+		PluginObj: types.Plugin{
311
+			Name:   name,
312
+			ID:     stringid.GenerateRandomID(),
313
+			Config: config,
314
+		},
315
+		Config:   configDigest,
316
+		Blobsums: blobsums,
317
+		Manifest: manifestDigest,
318
+	}
319
+	p.InitEmptySettings()
320
+	for _, o := range opts {
321
+		o(p)
322
+	}
323
+
324
+	pdir := filepath.Join(pm.config.Root, p.PluginObj.ID)
325
+	if err := os.MkdirAll(pdir, 0o700); err != nil {
326
+		return nil, errors.Wrapf(err, "failed to mkdir %v", pdir)
327
+	}
328
+
329
+	defer func() {
330
+		if retErr != nil {
331
+			_ = os.RemoveAll(pdir)
332
+		}
333
+	}()
334
+
335
+	if err := os.Rename(rootFSDir, filepath.Join(pdir, rootFSFileName)); err != nil {
336
+		return nil, errors.Wrap(err, "failed to rename rootfs")
337
+	}
338
+
339
+	if err := pm.save(p); err != nil {
340
+		return nil, err
341
+	}
342
+
343
+	pm.config.Store.Add(p) // todo: remove
344
+
345
+	return p, nil
346
+}
347
+
348
+func recursiveUnmount(target string) error {
349
+	return mount.RecursiveUnmount(target)
350
+}
0 351
new file mode 100644
... ...
@@ -0,0 +1,271 @@
0
+package plugin
1
+
2
+import (
3
+	"io"
4
+	"net"
5
+	"os"
6
+	"path/filepath"
7
+	"syscall"
8
+	"testing"
9
+
10
+	"github.com/docker/docker/api/types"
11
+	"github.com/docker/docker/api/types/backend"
12
+	"github.com/docker/docker/api/types/events"
13
+	v2 "github.com/docker/docker/daemon/pkg/plugin/v2"
14
+	"github.com/docker/docker/internal/containerfs"
15
+	"github.com/docker/docker/pkg/stringid"
16
+	"github.com/moby/sys/mount"
17
+	"github.com/moby/sys/mountinfo"
18
+	"github.com/opencontainers/runtime-spec/specs-go"
19
+	"github.com/pkg/errors"
20
+	"gotest.tools/v3/skip"
21
+)
22
+
23
+func TestManagerWithPluginMounts(t *testing.T) {
24
+	skip.If(t, os.Getuid() != 0, "skipping test that requires root")
25
+
26
+	root := t.TempDir()
27
+	t.Cleanup(func() { _ = containerfs.EnsureRemoveAll(root) })
28
+
29
+	s := NewStore()
30
+	managerRoot := filepath.Join(root, "manager")
31
+	p1 := newTestPlugin(t, "test1", "testcap", managerRoot)
32
+
33
+	p2 := newTestPlugin(t, "test2", "testcap", managerRoot)
34
+	p2.PluginObj.Enabled = true
35
+
36
+	m, err := NewManager(
37
+		ManagerConfig{
38
+			Store:          s,
39
+			Root:           managerRoot,
40
+			ExecRoot:       filepath.Join(root, "exec"),
41
+			CreateExecutor: func(*Manager) (Executor, error) { return nil, nil },
42
+			LogPluginEvent: func(_, _ string, _ events.Action) {},
43
+		})
44
+	if err != nil {
45
+		t.Fatal(err)
46
+	}
47
+
48
+	if err := s.Add(p1); err != nil {
49
+		t.Fatal(err)
50
+	}
51
+	if err := s.Add(p2); err != nil {
52
+		t.Fatal(err)
53
+	}
54
+
55
+	// Create a mount to simulate a plugin that has created it's own mounts
56
+	p2Mount := filepath.Join(p2.Rootfs, "testmount")
57
+	if err := os.MkdirAll(p2Mount, 0o755); err != nil {
58
+		t.Fatal(err)
59
+	}
60
+	if err := mount.Mount("tmpfs", p2Mount, "tmpfs", ""); err != nil {
61
+		t.Fatal(err)
62
+	}
63
+
64
+	if err := m.Remove(p1.GetID(), &backend.PluginRmConfig{ForceRemove: true}); err != nil {
65
+		t.Fatal(err)
66
+	}
67
+	if mounted, err := mountinfo.Mounted(p2Mount); !mounted || err != nil {
68
+		t.Fatalf("expected %s to be mounted, err: %v", p2Mount, err)
69
+	}
70
+}
71
+
72
+func newTestPlugin(t *testing.T, name, capability, root string) *v2.Plugin {
73
+	id := stringid.GenerateRandomID()
74
+	rootfs := filepath.Join(root, id)
75
+	if err := os.MkdirAll(rootfs, 0o755); err != nil {
76
+		t.Fatal(err)
77
+	}
78
+
79
+	p := v2.Plugin{PluginObj: types.Plugin{ID: id, Name: name}}
80
+	p.Rootfs = rootfs
81
+	iType := types.PluginInterfaceType{Capability: capability, Prefix: "docker", Version: "1.0"}
82
+	i := types.PluginConfigInterface{Socket: "plugin.sock", Types: []types.PluginInterfaceType{iType}}
83
+	p.PluginObj.Config.Interface = i
84
+	p.PluginObj.ID = id
85
+
86
+	return &p
87
+}
88
+
89
+type simpleExecutor struct {
90
+	Executor
91
+}
92
+
93
+func (e *simpleExecutor) Create(id string, spec specs.Spec, stdout, stderr io.WriteCloser) error {
94
+	return errors.New("Create failed")
95
+}
96
+
97
+func TestCreateFailed(t *testing.T) {
98
+	root, err := os.MkdirTemp("", "test-create-failed")
99
+	if err != nil {
100
+		t.Fatal(err)
101
+	}
102
+	defer containerfs.EnsureRemoveAll(root)
103
+
104
+	s := NewStore()
105
+	managerRoot := filepath.Join(root, "manager")
106
+	p := newTestPlugin(t, "create", "testcreate", managerRoot)
107
+
108
+	m, err := NewManager(
109
+		ManagerConfig{
110
+			Store:          s,
111
+			Root:           managerRoot,
112
+			ExecRoot:       filepath.Join(root, "exec"),
113
+			CreateExecutor: func(*Manager) (Executor, error) { return &simpleExecutor{}, nil },
114
+			LogPluginEvent: func(_, _ string, _ events.Action) {},
115
+		})
116
+	if err != nil {
117
+		t.Fatal(err)
118
+	}
119
+
120
+	if err := s.Add(p); err != nil {
121
+		t.Fatal(err)
122
+	}
123
+
124
+	if err := m.enable(p, &controller{}, false); err == nil {
125
+		t.Fatalf("expected Create failed error, got %v", err)
126
+	}
127
+
128
+	if err := m.Remove(p.GetID(), &backend.PluginRmConfig{ForceRemove: true}); err != nil {
129
+		t.Fatal(err)
130
+	}
131
+}
132
+
133
+type executorWithRunning struct {
134
+	m         *Manager
135
+	root      string
136
+	exitChans map[string]chan struct{}
137
+}
138
+
139
+func (e *executorWithRunning) Create(id string, spec specs.Spec, stdout, stderr io.WriteCloser) error {
140
+	sockAddr := filepath.Join(e.root, id, "plugin.sock")
141
+	ch := make(chan struct{})
142
+	if e.exitChans == nil {
143
+		e.exitChans = make(map[string]chan struct{})
144
+	}
145
+	e.exitChans[id] = ch
146
+	listenTestPlugin(sockAddr, ch)
147
+	return nil
148
+}
149
+
150
+func (e *executorWithRunning) IsRunning(id string) (bool, error) {
151
+	return true, nil
152
+}
153
+
154
+func (e *executorWithRunning) Restore(id string, stdout, stderr io.WriteCloser) (bool, error) {
155
+	return true, nil
156
+}
157
+
158
+func (e *executorWithRunning) Signal(id string, signal syscall.Signal) error {
159
+	ch := e.exitChans[id]
160
+	ch <- struct{}{}
161
+	<-ch
162
+	e.m.HandleExitEvent(id)
163
+	return nil
164
+}
165
+
166
+func TestPluginAlreadyRunningOnStartup(t *testing.T) {
167
+	skip.If(t, os.Getuid() != 0, "skipping test that requires root")
168
+	t.Parallel()
169
+
170
+	root, err := os.MkdirTemp("", t.Name())
171
+	if err != nil {
172
+		t.Fatal(err)
173
+	}
174
+	defer containerfs.EnsureRemoveAll(root)
175
+
176
+	for _, test := range []struct {
177
+		desc   string
178
+		config ManagerConfig
179
+	}{
180
+		{
181
+			desc: "live-restore-disabled",
182
+			config: ManagerConfig{
183
+				LogPluginEvent: func(_, _ string, _ events.Action) {},
184
+			},
185
+		},
186
+		{
187
+			desc: "live-restore-enabled",
188
+			config: ManagerConfig{
189
+				LogPluginEvent:     func(_, _ string, _ events.Action) {},
190
+				LiveRestoreEnabled: true,
191
+			},
192
+		},
193
+	} {
194
+		t.Run(test.desc, func(t *testing.T) {
195
+			config := test.config
196
+			desc := test.desc
197
+			t.Parallel()
198
+
199
+			p := newTestPlugin(t, desc, desc, config.Root)
200
+			p.PluginObj.Enabled = true
201
+
202
+			// Need a short-ish path here so we don't run into unix socket path length issues.
203
+			config.ExecRoot, err = os.MkdirTemp("", "plugintest")
204
+
205
+			executor := &executorWithRunning{root: config.ExecRoot}
206
+			config.CreateExecutor = func(m *Manager) (Executor, error) { executor.m = m; return executor, nil }
207
+
208
+			if err := executor.Create(p.GetID(), specs.Spec{}, nil, nil); err != nil {
209
+				t.Fatal(err)
210
+			}
211
+
212
+			config.Root = filepath.Join(root, desc, "manager")
213
+			if err := os.MkdirAll(filepath.Join(config.Root, p.GetID()), 0o755); err != nil {
214
+				t.Fatal(err)
215
+			}
216
+
217
+			if !p.IsEnabled() {
218
+				t.Fatal("plugin should be enabled")
219
+			}
220
+			if err := (&Manager{config: config}).save(p); err != nil {
221
+				t.Fatal(err)
222
+			}
223
+
224
+			s := NewStore()
225
+			config.Store = s
226
+			if err != nil {
227
+				t.Fatal(err)
228
+			}
229
+			defer containerfs.EnsureRemoveAll(config.ExecRoot)
230
+
231
+			m, err := NewManager(config)
232
+			if err != nil {
233
+				t.Fatal(err)
234
+			}
235
+			defer m.Shutdown()
236
+
237
+			p = s.GetAll()[p.GetID()] // refresh `p` with what the manager knows
238
+
239
+			if p.Client() == nil { //nolint:staticcheck // FIXME(thaJeztah): p.Client is deprecated: use p.Addr() and manually create the client
240
+				t.Fatal("plugin client should not be nil")
241
+			}
242
+		})
243
+	}
244
+}
245
+
246
+func listenTestPlugin(sockAddr string, exit chan struct{}) (net.Listener, error) {
247
+	if err := os.MkdirAll(filepath.Dir(sockAddr), 0o755); err != nil {
248
+		return nil, err
249
+	}
250
+	l, err := net.Listen("unix", sockAddr)
251
+	if err != nil {
252
+		return nil, err
253
+	}
254
+	go func() {
255
+		for {
256
+			conn, err := l.Accept()
257
+			if err != nil {
258
+				return
259
+			}
260
+			conn.Close()
261
+		}
262
+	}()
263
+	go func() {
264
+		<-exit
265
+		l.Close()
266
+		os.Remove(sockAddr)
267
+		exit <- struct{}{}
268
+	}()
269
+	return l, nil
270
+}
0 271
new file mode 100644
... ...
@@ -0,0 +1,55 @@
0
+package plugin
1
+
2
+import (
3
+	"testing"
4
+
5
+	"github.com/docker/docker/api/types"
6
+)
7
+
8
+func TestValidatePrivileges(t *testing.T) {
9
+	testData := map[string]struct {
10
+		requiredPrivileges types.PluginPrivileges
11
+		privileges         types.PluginPrivileges
12
+		result             bool
13
+	}{
14
+		"diff-len": {
15
+			requiredPrivileges: []types.PluginPrivilege{
16
+				{Name: "Privilege1", Description: "Description", Value: []string{"abc", "def", "ghi"}},
17
+			},
18
+			privileges: []types.PluginPrivilege{
19
+				{Name: "Privilege1", Description: "Description", Value: []string{"abc", "def", "ghi"}},
20
+				{Name: "Privilege2", Description: "Description", Value: []string{"123", "456", "789"}},
21
+			},
22
+			result: false,
23
+		},
24
+		"diff-value": {
25
+			requiredPrivileges: []types.PluginPrivilege{
26
+				{Name: "Privilege1", Description: "Description", Value: []string{"abc", "def", "GHI"}},
27
+				{Name: "Privilege2", Description: "Description", Value: []string{"123", "456", "***"}},
28
+			},
29
+			privileges: []types.PluginPrivilege{
30
+				{Name: "Privilege1", Description: "Description", Value: []string{"abc", "def", "ghi"}},
31
+				{Name: "Privilege2", Description: "Description", Value: []string{"123", "456", "789"}},
32
+			},
33
+			result: false,
34
+		},
35
+		"diff-order-but-same-value": {
36
+			requiredPrivileges: []types.PluginPrivilege{
37
+				{Name: "Privilege1", Description: "Description", Value: []string{"abc", "def", "GHI"}},
38
+				{Name: "Privilege2", Description: "Description", Value: []string{"123", "456", "789"}},
39
+			},
40
+			privileges: []types.PluginPrivilege{
41
+				{Name: "Privilege2", Description: "Description", Value: []string{"123", "456", "789"}},
42
+				{Name: "Privilege1", Description: "Description", Value: []string{"GHI", "abc", "def"}},
43
+			},
44
+			result: true,
45
+		},
46
+	}
47
+
48
+	for key, data := range testData {
49
+		err := validatePrivileges(data.requiredPrivileges, data.privileges)
50
+		if (err == nil) != data.result {
51
+			t.Fatalf("Test item %s expected result to be %t, got %t", key, data.result, (err == nil))
52
+		}
53
+	}
54
+}
0 55
new file mode 100644
... ...
@@ -0,0 +1,32 @@
0
+package plugin
1
+
2
+import (
3
+	"fmt"
4
+
5
+	v2 "github.com/docker/docker/daemon/pkg/plugin/v2"
6
+	"github.com/opencontainers/runtime-spec/specs-go"
7
+)
8
+
9
+func (pm *Manager) enable(p *v2.Plugin, c *controller, force bool) error {
10
+	return fmt.Errorf("Not implemented")
11
+}
12
+
13
+func (pm *Manager) initSpec(p *v2.Plugin) (*specs.Spec, error) {
14
+	return nil, fmt.Errorf("Not implemented")
15
+}
16
+
17
+func (pm *Manager) disable(p *v2.Plugin, c *controller) error {
18
+	return fmt.Errorf("Not implemented")
19
+}
20
+
21
+func (pm *Manager) restore(p *v2.Plugin, c *controller) error {
22
+	return fmt.Errorf("Not implemented")
23
+}
24
+
25
+// Shutdown plugins
26
+func (pm *Manager) Shutdown() {
27
+}
28
+
29
+func recursiveUnmount(_ string) error {
30
+	return nil
31
+}
0 32
new file mode 100644
... ...
@@ -0,0 +1,74 @@
0
+package plugin
1
+
2
+import (
3
+	"sync"
4
+	"time"
5
+
6
+	"github.com/containerd/containerd/v2/core/remotes/docker"
7
+)
8
+
9
+func newPushJobs(tracker docker.StatusTracker) *pushJobs {
10
+	return &pushJobs{
11
+		names: make(map[string]string),
12
+		t:     tracker,
13
+	}
14
+}
15
+
16
+type pushJobs struct {
17
+	t docker.StatusTracker
18
+
19
+	mu   sync.Mutex
20
+	jobs []string
21
+	// maps job ref to a name
22
+	names map[string]string
23
+}
24
+
25
+func (p *pushJobs) add(id, name string) {
26
+	p.mu.Lock()
27
+	defer p.mu.Unlock()
28
+
29
+	if _, ok := p.names[id]; ok {
30
+		return
31
+	}
32
+	p.jobs = append(p.jobs, id)
33
+	p.names[id] = name
34
+}
35
+
36
+func (p *pushJobs) status() []contentStatus {
37
+	statuses := make([]contentStatus, 0, len(p.jobs))
38
+
39
+	p.mu.Lock()
40
+	defer p.mu.Unlock()
41
+
42
+	for _, j := range p.jobs {
43
+		var s contentStatus
44
+		s.Ref = p.names[j]
45
+
46
+		status, err := p.t.GetStatus(j)
47
+		if err != nil {
48
+			s.Status = "Waiting"
49
+		} else {
50
+			s.Total = status.Total
51
+			s.Offset = status.Offset
52
+			s.StartedAt = status.StartedAt
53
+			s.UpdatedAt = status.UpdatedAt
54
+			if status.UploadUUID == "" {
55
+				s.Status = "Upload complete"
56
+			} else {
57
+				s.Status = "Uploading"
58
+			}
59
+		}
60
+		statuses = append(statuses, s)
61
+	}
62
+
63
+	return statuses
64
+}
65
+
66
+type contentStatus struct {
67
+	Status    string
68
+	Total     int64
69
+	Offset    int64
70
+	StartedAt time.Time
71
+	UpdatedAt time.Time
72
+	Ref       string
73
+}
0 74
new file mode 100644
... ...
@@ -0,0 +1,108 @@
0
+package plugin
1
+
2
+import (
3
+	"context"
4
+	"crypto/tls"
5
+	"net"
6
+	"net/http"
7
+	"time"
8
+
9
+	"github.com/containerd/containerd/v2/core/remotes"
10
+	"github.com/containerd/containerd/v2/core/remotes/docker"
11
+	"github.com/containerd/log"
12
+	"github.com/distribution/reference"
13
+	"github.com/docker/docker/api/types/registry"
14
+	"github.com/docker/docker/dockerversion"
15
+	"github.com/pkg/errors"
16
+)
17
+
18
+// scope builds the correct auth scope for the registry client to authorize against
19
+// By default the client currently only does a "repository:" scope with out a classifier, e.g. "(plugin)"
20
+// Without this, the client will not be able to authorize the request
21
+func scope(ref reference.Named, push bool) string {
22
+	scope := "repository(plugin):" + reference.Path(reference.TrimNamed(ref)) + ":pull"
23
+	if push {
24
+		scope += ",push"
25
+	}
26
+	return scope
27
+}
28
+
29
+func (pm *Manager) newResolver(ctx context.Context, tracker docker.StatusTracker, auth *registry.AuthConfig, headers http.Header, httpFallback bool) (remotes.Resolver, error) {
30
+	if headers == nil {
31
+		headers = http.Header{}
32
+	}
33
+	headers.Add("User-Agent", dockerversion.DockerUserAgent(ctx))
34
+
35
+	return docker.NewResolver(docker.ResolverOptions{
36
+		Tracker: tracker,
37
+		Headers: headers,
38
+		Hosts:   pm.registryHostsFn(auth, httpFallback),
39
+	}), nil
40
+}
41
+
42
+func registryHTTPClient(config *tls.Config) *http.Client {
43
+	return &http.Client{
44
+		Transport: &http.Transport{
45
+			Proxy: http.ProxyFromEnvironment,
46
+			DialContext: (&net.Dialer{
47
+				Timeout:   30 * time.Second,
48
+				KeepAlive: 30 * time.Second,
49
+			}).DialContext,
50
+			TLSClientConfig:     config,
51
+			TLSHandshakeTimeout: 10 * time.Second,
52
+			IdleConnTimeout:     30 * time.Second,
53
+		},
54
+	}
55
+}
56
+
57
+func (pm *Manager) registryHostsFn(auth *registry.AuthConfig, httpFallback bool) docker.RegistryHosts {
58
+	return func(hostname string) ([]docker.RegistryHost, error) {
59
+		eps, err := pm.config.RegistryService.LookupPullEndpoints(hostname)
60
+		if err != nil {
61
+			return nil, errors.Wrapf(err, "error resolving repository for %s", hostname)
62
+		}
63
+
64
+		hosts := make([]docker.RegistryHost, 0, len(eps))
65
+
66
+		for _, ep := range eps {
67
+			// forced http fallback is used only for push since the containerd pusher only ever uses the first host we
68
+			// pass to it.
69
+			// So it is the callers responsibility to retry with this flag set.
70
+			if httpFallback && ep.URL.Scheme != "http" {
71
+				log.G(context.TODO()).WithField("registryHost", hostname).WithField("endpoint", ep).Debugf("Skipping non-http endpoint")
72
+				continue
73
+			}
74
+
75
+			caps := docker.HostCapabilityPull | docker.HostCapabilityResolve
76
+			if !ep.Mirror {
77
+				caps = caps | docker.HostCapabilityPush
78
+			}
79
+
80
+			host, err := docker.DefaultHost(ep.URL.Host)
81
+			if err != nil {
82
+				return nil, err
83
+			}
84
+
85
+			client := registryHTTPClient(ep.TLSConfig)
86
+			hosts = append(hosts, docker.RegistryHost{
87
+				Host:         host,
88
+				Scheme:       ep.URL.Scheme,
89
+				Client:       client,
90
+				Path:         "/v2",
91
+				Capabilities: caps,
92
+				Authorizer: docker.NewDockerAuthorizer(
93
+					docker.WithAuthClient(client),
94
+					docker.WithAuthCreds(func(_ string) (string, string, error) {
95
+						if auth.IdentityToken != "" {
96
+							return "", auth.IdentityToken, nil
97
+						}
98
+						return auth.Username, auth.Password, nil
99
+					}),
100
+				),
101
+			})
102
+		}
103
+		log.G(context.TODO()).WithField("registryHost", hostname).WithField("hosts", hosts).Debug("Resolved registry hosts")
104
+
105
+		return hosts, nil
106
+	}
107
+}
0 108
new file mode 100644
... ...
@@ -0,0 +1,288 @@
0
+package plugin
1
+
2
+import (
3
+	"context"
4
+	"fmt"
5
+	"strings"
6
+
7
+	"github.com/containerd/log"
8
+	"github.com/distribution/reference"
9
+	v2 "github.com/docker/docker/daemon/pkg/plugin/v2"
10
+	"github.com/docker/docker/errdefs"
11
+	"github.com/docker/docker/pkg/plugingetter"
12
+	"github.com/docker/docker/pkg/plugins"
13
+	"github.com/opencontainers/runtime-spec/specs-go"
14
+	"github.com/pkg/errors"
15
+)
16
+
17
+// allowV1PluginsFallback determines daemon's support for V1 plugins.
18
+// When the time comes to remove support for V1 plugins, flipping
19
+// this bool is all that will be needed.
20
+const allowV1PluginsFallback = true
21
+
22
+// defaultAPIVersion is the version of the plugin API for volume, network,
23
+// IPAM and authz. This is a very stable API. When we update this API, then
24
+// pluginType should include a version. e.g. "networkdriver/2.0".
25
+const defaultAPIVersion = "1.0"
26
+
27
+// GetV2Plugin retrieves a plugin by name, id or partial ID.
28
+func (ps *Store) GetV2Plugin(refOrID string) (*v2.Plugin, error) {
29
+	ps.RLock()
30
+	defer ps.RUnlock()
31
+
32
+	id, err := ps.resolvePluginID(refOrID)
33
+	if err != nil {
34
+		return nil, err
35
+	}
36
+
37
+	p, idOk := ps.plugins[id]
38
+	if !idOk {
39
+		return nil, errors.WithStack(errNotFound(id))
40
+	}
41
+
42
+	return p, nil
43
+}
44
+
45
+// validateName returns error if name is already reserved. always call with lock and full name
46
+func (ps *Store) validateName(name string) error {
47
+	for _, p := range ps.plugins {
48
+		if p.Name() == name {
49
+			return alreadyExistsError(name)
50
+		}
51
+	}
52
+	return nil
53
+}
54
+
55
+// GetAll retrieves all plugins.
56
+func (ps *Store) GetAll() map[string]*v2.Plugin {
57
+	ps.RLock()
58
+	defer ps.RUnlock()
59
+	return ps.plugins
60
+}
61
+
62
+// SetAll initialized plugins during daemon restore.
63
+func (ps *Store) SetAll(plugins map[string]*v2.Plugin) {
64
+	ps.Lock()
65
+	defer ps.Unlock()
66
+
67
+	for _, p := range plugins {
68
+		ps.setSpecOpts(p)
69
+	}
70
+	ps.plugins = plugins
71
+}
72
+
73
+func (ps *Store) getAllByCap(capability string) []plugingetter.CompatPlugin {
74
+	ps.RLock()
75
+	defer ps.RUnlock()
76
+
77
+	result := make([]plugingetter.CompatPlugin, 0, 1)
78
+	for _, p := range ps.plugins {
79
+		if p.IsEnabled() {
80
+			if _, err := p.FilterByCap(capability); err == nil {
81
+				result = append(result, p)
82
+			}
83
+		}
84
+	}
85
+	return result
86
+}
87
+
88
+// SetState sets the active state of the plugin and updates plugindb.
89
+func (ps *Store) SetState(p *v2.Plugin, state bool) {
90
+	ps.Lock()
91
+	defer ps.Unlock()
92
+
93
+	p.PluginObj.Enabled = state
94
+}
95
+
96
+func (ps *Store) setSpecOpts(p *v2.Plugin) {
97
+	var specOpts []SpecOpt
98
+	for _, typ := range p.GetTypes() {
99
+		opts, ok := ps.specOpts[typ.String()]
100
+		if ok {
101
+			specOpts = append(specOpts, opts...)
102
+		}
103
+	}
104
+
105
+	p.SetSpecOptModifier(func(s *specs.Spec) {
106
+		for _, o := range specOpts {
107
+			o(s)
108
+		}
109
+	})
110
+}
111
+
112
+// Add adds a plugin to memory and plugindb.
113
+// An error will be returned if there is a collision.
114
+func (ps *Store) Add(p *v2.Plugin) error {
115
+	ps.Lock()
116
+	defer ps.Unlock()
117
+
118
+	if v, exist := ps.plugins[p.GetID()]; exist {
119
+		return fmt.Errorf("plugin %q has the same ID %s as %q", p.Name(), p.GetID(), v.Name())
120
+	}
121
+
122
+	ps.setSpecOpts(p)
123
+
124
+	ps.plugins[p.GetID()] = p
125
+	return nil
126
+}
127
+
128
+// Remove removes a plugin from memory and plugindb.
129
+func (ps *Store) Remove(p *v2.Plugin) {
130
+	ps.Lock()
131
+	delete(ps.plugins, p.GetID())
132
+	ps.Unlock()
133
+}
134
+
135
+// Get returns an enabled plugin matching the given name and capability.
136
+func (ps *Store) Get(name, capability string, mode int) (plugingetter.CompatPlugin, error) {
137
+	// Lookup using new model.
138
+	if ps != nil {
139
+		p, err := ps.GetV2Plugin(name)
140
+		if err == nil {
141
+			if p.IsEnabled() {
142
+				fp, err := p.FilterByCap(capability)
143
+				if err != nil {
144
+					return nil, err
145
+				}
146
+				p.AddRefCount(mode)
147
+				return fp, nil
148
+			}
149
+
150
+			// Plugin was found but it is disabled, so we should not fall back to legacy plugins
151
+			// but we should error out right away
152
+			return nil, errDisabled(name)
153
+		}
154
+		var ierr errNotFound
155
+		if !errors.As(err, &ierr) {
156
+			return nil, err
157
+		}
158
+	}
159
+
160
+	if !allowV1PluginsFallback {
161
+		return nil, errNotFound(name)
162
+	}
163
+
164
+	p, err := plugins.Get(name, capability)
165
+	if err == nil {
166
+		return p, nil
167
+	}
168
+	if errors.Is(err, plugins.ErrNotFound) {
169
+		return nil, errNotFound(name)
170
+	}
171
+	return nil, errors.Wrap(errdefs.System(err), "legacy plugin")
172
+}
173
+
174
+// GetAllManagedPluginsByCap returns a list of managed plugins matching the given capability.
175
+func (ps *Store) GetAllManagedPluginsByCap(capability string) []plugingetter.CompatPlugin {
176
+	return ps.getAllByCap(capability)
177
+}
178
+
179
+// GetAllByCap returns a list of enabled plugins matching the given capability.
180
+func (ps *Store) GetAllByCap(capability string) ([]plugingetter.CompatPlugin, error) {
181
+	result := make([]plugingetter.CompatPlugin, 0, 1)
182
+
183
+	/* Daemon start always calls plugin.Init thereby initializing a store.
184
+	 * So store on experimental builds can never be nil, even while
185
+	 * handling legacy plugins. However, there are legacy plugin unit
186
+	 * tests where the volume subsystem directly talks with the plugin,
187
+	 * bypassing the daemon. For such tests, this check is necessary.
188
+	 */
189
+	if ps != nil {
190
+		result = ps.getAllByCap(capability)
191
+	}
192
+
193
+	// Lookup with legacy model
194
+	if allowV1PluginsFallback {
195
+		l := plugins.NewLocalRegistry()
196
+		pl, err := l.GetAll(capability)
197
+		if err != nil {
198
+			return nil, errors.Wrap(errdefs.System(err), "legacy plugin")
199
+		}
200
+		for _, p := range pl {
201
+			result = append(result, p)
202
+		}
203
+	}
204
+	return result, nil
205
+}
206
+
207
+func pluginType(capability string) string {
208
+	return fmt.Sprintf("docker.%s/%s", strings.ToLower(capability), defaultAPIVersion)
209
+}
210
+
211
+// Handle sets a callback for a given capability. It is only used by network
212
+// and ipam drivers during plugin registration. The callback registers the
213
+// driver with the subsystem (network, ipam).
214
+func (ps *Store) Handle(capability string, callback func(string, *plugins.Client)) {
215
+	typ := pluginType(capability)
216
+
217
+	// Register callback with new plugin model.
218
+	ps.Lock()
219
+	handlers, ok := ps.handlers[typ]
220
+	if !ok {
221
+		handlers = []func(string, *plugins.Client){}
222
+	}
223
+	handlers = append(handlers, callback)
224
+	ps.handlers[typ] = handlers
225
+	ps.Unlock()
226
+
227
+	// Register callback with legacy plugin model.
228
+	if allowV1PluginsFallback {
229
+		plugins.Handle(capability, callback)
230
+	}
231
+}
232
+
233
+// RegisterRuntimeOpt stores a list of SpecOpts for the provided capability.
234
+// These options are applied to the runtime spec before a plugin is started for the specified capability.
235
+func (ps *Store) RegisterRuntimeOpt(capability string, opts ...SpecOpt) {
236
+	ps.Lock()
237
+	defer ps.Unlock()
238
+	typ := pluginType(capability)
239
+	ps.specOpts[typ] = append(ps.specOpts[typ], opts...)
240
+}
241
+
242
+// CallHandler calls the registered callback. It is invoked during plugin enable.
243
+func (ps *Store) CallHandler(p *v2.Plugin) {
244
+	for _, typ := range p.GetTypes() {
245
+		for _, handler := range ps.handlers[typ.String()] {
246
+			handler(p.Name(), p.Client()) //nolint:staticcheck // FIXME(thaJeztah): p.Client is deprecated: use p.Addr() and manually create the client
247
+		}
248
+	}
249
+}
250
+
251
+// resolvePluginID must be protected by ps.RLock
252
+func (ps *Store) resolvePluginID(idOrName string) (string, error) {
253
+	if validFullID.MatchString(idOrName) {
254
+		return idOrName, nil
255
+	}
256
+
257
+	ref, err := reference.ParseNormalizedNamed(idOrName)
258
+	if err != nil {
259
+		return "", errors.WithStack(errNotFound(idOrName))
260
+	}
261
+	if _, ok := ref.(reference.Canonical); ok {
262
+		log.G(context.TODO()).Warnf("canonical references cannot be resolved: %v", reference.FamiliarString(ref))
263
+		return "", errors.WithStack(errNotFound(idOrName))
264
+	}
265
+
266
+	ref = reference.TagNameOnly(ref)
267
+
268
+	for _, p := range ps.plugins {
269
+		if p.PluginObj.Name == reference.FamiliarString(ref) {
270
+			return p.PluginObj.ID, nil
271
+		}
272
+	}
273
+
274
+	var found *v2.Plugin
275
+	for id, p := range ps.plugins { // this can be optimized
276
+		if strings.HasPrefix(id, idOrName) {
277
+			if found != nil {
278
+				return "", errors.WithStack(errAmbiguous(idOrName))
279
+			}
280
+			found = p
281
+		}
282
+	}
283
+	if found == nil {
284
+		return "", errors.WithStack(errNotFound(idOrName))
285
+	}
286
+	return found.PluginObj.ID, nil
287
+}
0 288
new file mode 100644
... ...
@@ -0,0 +1,64 @@
0
+package plugin
1
+
2
+import (
3
+	"testing"
4
+
5
+	"github.com/docker/docker/api/types"
6
+	v2 "github.com/docker/docker/daemon/pkg/plugin/v2"
7
+	"github.com/docker/docker/pkg/plugingetter"
8
+)
9
+
10
+func TestFilterByCapNeg(t *testing.T) {
11
+	p := v2.Plugin{PluginObj: types.Plugin{Name: "test:latest"}}
12
+	iType := types.PluginInterfaceType{Capability: "volumedriver", Prefix: "docker", Version: "1.0"}
13
+	i := types.PluginConfigInterface{Socket: "plugins.sock", Types: []types.PluginInterfaceType{iType}}
14
+	p.PluginObj.Config.Interface = i
15
+
16
+	_, err := p.FilterByCap("foobar")
17
+	if err == nil {
18
+		t.Fatalf("expected inadequate error, got %v", err)
19
+	}
20
+}
21
+
22
+func TestFilterByCapPos(t *testing.T) {
23
+	p := v2.Plugin{PluginObj: types.Plugin{Name: "test:latest"}}
24
+
25
+	iType := types.PluginInterfaceType{Capability: "volumedriver", Prefix: "docker", Version: "1.0"}
26
+	i := types.PluginConfigInterface{Socket: "plugins.sock", Types: []types.PluginInterfaceType{iType}}
27
+	p.PluginObj.Config.Interface = i
28
+
29
+	_, err := p.FilterByCap("volumedriver")
30
+	if err != nil {
31
+		t.Fatalf("expected no error, got %v", err)
32
+	}
33
+}
34
+
35
+func TestStoreGetPluginNotMatchCapRefs(t *testing.T) {
36
+	s := NewStore()
37
+	p := v2.Plugin{PluginObj: types.Plugin{Name: "test:latest"}}
38
+
39
+	iType := types.PluginInterfaceType{Capability: "whatever", Prefix: "docker", Version: "1.0"}
40
+	i := types.PluginConfigInterface{Socket: "plugins.sock", Types: []types.PluginInterfaceType{iType}}
41
+	p.PluginObj.Config.Interface = i
42
+
43
+	if err := s.Add(&p); err != nil {
44
+		t.Fatal(err)
45
+	}
46
+
47
+	if _, err := s.Get("test", "volumedriver", plugingetter.Acquire); err == nil {
48
+		t.Fatal("expected error when getting plugin that doesn't match the passed in capability")
49
+	}
50
+
51
+	if refs := p.GetRefCount(); refs != 0 {
52
+		t.Fatalf("reference count should be 0, got: %d", refs)
53
+	}
54
+
55
+	p.PluginObj.Enabled = true
56
+	if _, err := s.Get("test", "volumedriver", plugingetter.Acquire); err == nil {
57
+		t.Fatal("expected error when getting plugin that doesn't match the passed in capability")
58
+	}
59
+
60
+	if refs := p.GetRefCount(); refs != 0 {
61
+		t.Fatalf("reference count should be 0, got: %d", refs)
62
+	}
63
+}
... ...
@@ -10,7 +10,7 @@ import (
10 10
 	"github.com/docker/docker/api/types/backend"
11 11
 	"github.com/docker/docker/api/types/filters"
12 12
 	"github.com/docker/docker/api/types/registry"
13
-	"github.com/docker/docker/plugin"
13
+	"github.com/docker/docker/daemon/pkg/plugin"
14 14
 )
15 15
 
16 16
 // Backend for Plugin
17 17
deleted file mode 100644
... ...
@@ -1,836 +0,0 @@
1
-package plugin
2
-
3
-import (
4
-	"archive/tar"
5
-	"bytes"
6
-	"compress/gzip"
7
-	"context"
8
-	"encoding/json"
9
-	"io"
10
-	"net/http"
11
-	"os"
12
-	"path"
13
-	"path/filepath"
14
-	"strings"
15
-	"time"
16
-
17
-	"github.com/containerd/containerd/v2/core/content"
18
-	c8dimages "github.com/containerd/containerd/v2/core/images"
19
-	"github.com/containerd/containerd/v2/core/remotes"
20
-	"github.com/containerd/containerd/v2/core/remotes/docker"
21
-	"github.com/containerd/log"
22
-	"github.com/containerd/platforms"
23
-	"github.com/distribution/reference"
24
-	"github.com/docker/distribution/manifest/schema2"
25
-	"github.com/docker/docker/api/types"
26
-	"github.com/docker/docker/api/types/backend"
27
-	"github.com/docker/docker/api/types/events"
28
-	"github.com/docker/docker/api/types/filters"
29
-	"github.com/docker/docker/api/types/registry"
30
-	v2 "github.com/docker/docker/daemon/pkg/plugin/v2"
31
-	"github.com/docker/docker/dockerversion"
32
-	"github.com/docker/docker/errdefs"
33
-	"github.com/docker/docker/internal/containerfs"
34
-	"github.com/docker/docker/pkg/authorization"
35
-	"github.com/docker/docker/pkg/pools"
36
-	"github.com/docker/docker/pkg/progress"
37
-	"github.com/docker/docker/pkg/stringid"
38
-	"github.com/moby/go-archive/chrootarchive"
39
-	"github.com/moby/sys/mount"
40
-	"github.com/opencontainers/go-digest"
41
-	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
42
-	"github.com/pkg/errors"
43
-)
44
-
45
-var acceptedPluginFilterTags = map[string]bool{
46
-	"enabled":    true,
47
-	"capability": true,
48
-}
49
-
50
-// Disable deactivates a plugin. This means resources (volumes, networks) cant use them.
51
-func (pm *Manager) Disable(refOrID string, config *backend.PluginDisableConfig) error {
52
-	p, err := pm.config.Store.GetV2Plugin(refOrID)
53
-	if err != nil {
54
-		return err
55
-	}
56
-	pm.mu.RLock()
57
-	c := pm.cMap[p]
58
-	pm.mu.RUnlock()
59
-
60
-	if !config.ForceDisable && p.GetRefCount() > 0 {
61
-		return errors.WithStack(inUseError(p.Name()))
62
-	}
63
-
64
-	for _, typ := range p.GetTypes() {
65
-		if typ.Capability == authorization.AuthZApiImplements {
66
-			pm.config.AuthzMiddleware.RemovePlugin(p.Name())
67
-		}
68
-	}
69
-
70
-	if err := pm.disable(p, c); err != nil {
71
-		return err
72
-	}
73
-	pm.publisher.Publish(EventDisable{Plugin: p.PluginObj})
74
-	pm.config.LogPluginEvent(p.GetID(), refOrID, events.ActionDisable)
75
-	return nil
76
-}
77
-
78
-// Enable activates a plugin, which implies that they are ready to be used by containers.
79
-func (pm *Manager) Enable(refOrID string, config *backend.PluginEnableConfig) error {
80
-	p, err := pm.config.Store.GetV2Plugin(refOrID)
81
-	if err != nil {
82
-		return err
83
-	}
84
-
85
-	c := &controller{timeoutInSecs: config.Timeout}
86
-	if err := pm.enable(p, c, false); err != nil {
87
-		return err
88
-	}
89
-	pm.publisher.Publish(EventEnable{Plugin: p.PluginObj})
90
-	pm.config.LogPluginEvent(p.GetID(), refOrID, events.ActionEnable)
91
-	return nil
92
-}
93
-
94
-// Inspect examines a plugin config
95
-func (pm *Manager) Inspect(refOrID string) (*types.Plugin, error) {
96
-	p, err := pm.config.Store.GetV2Plugin(refOrID)
97
-	if err != nil {
98
-		return nil, err
99
-	}
100
-
101
-	return &p.PluginObj, nil
102
-}
103
-
104
-func computePrivileges(c types.PluginConfig) types.PluginPrivileges {
105
-	var privileges types.PluginPrivileges
106
-	if c.Network.Type != "null" && c.Network.Type != "bridge" && c.Network.Type != "" {
107
-		privileges = append(privileges, types.PluginPrivilege{
108
-			Name:        "network",
109
-			Description: "permissions to access a network",
110
-			Value:       []string{c.Network.Type},
111
-		})
112
-	}
113
-	if c.IpcHost {
114
-		privileges = append(privileges, types.PluginPrivilege{
115
-			Name:        "host ipc namespace",
116
-			Description: "allow access to host ipc namespace",
117
-			Value:       []string{"true"},
118
-		})
119
-	}
120
-	if c.PidHost {
121
-		privileges = append(privileges, types.PluginPrivilege{
122
-			Name:        "host pid namespace",
123
-			Description: "allow access to host pid namespace",
124
-			Value:       []string{"true"},
125
-		})
126
-	}
127
-	for _, mnt := range c.Mounts {
128
-		if mnt.Source != nil {
129
-			privileges = append(privileges, types.PluginPrivilege{
130
-				Name:        "mount",
131
-				Description: "host path to mount",
132
-				Value:       []string{*mnt.Source},
133
-			})
134
-		}
135
-	}
136
-	for _, device := range c.Linux.Devices {
137
-		if device.Path != nil {
138
-			privileges = append(privileges, types.PluginPrivilege{
139
-				Name:        "device",
140
-				Description: "host device to access",
141
-				Value:       []string{*device.Path},
142
-			})
143
-		}
144
-	}
145
-	if c.Linux.AllowAllDevices {
146
-		privileges = append(privileges, types.PluginPrivilege{
147
-			Name:        "allow-all-devices",
148
-			Description: "allow 'rwm' access to all devices",
149
-			Value:       []string{"true"},
150
-		})
151
-	}
152
-	if len(c.Linux.Capabilities) > 0 {
153
-		privileges = append(privileges, types.PluginPrivilege{
154
-			Name:        "capabilities",
155
-			Description: "list of additional capabilities required",
156
-			Value:       c.Linux.Capabilities,
157
-		})
158
-	}
159
-
160
-	return privileges
161
-}
162
-
163
-// Privileges pulls a plugin config and computes the privileges required to install it.
164
-func (pm *Manager) Privileges(ctx context.Context, ref reference.Named, metaHeader http.Header, authConfig *registry.AuthConfig) (types.PluginPrivileges, error) {
165
-	var (
166
-		config     types.PluginConfig
167
-		configSeen bool
168
-	)
169
-
170
-	h := func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
171
-		switch desc.MediaType {
172
-		case schema2.MediaTypeManifest, ocispec.MediaTypeImageManifest:
173
-			data, err := content.ReadBlob(ctx, pm.blobStore, desc)
174
-			if err != nil {
175
-				return nil, errors.Wrapf(err, "error reading image manifest from blob store for %s", ref)
176
-			}
177
-
178
-			var m ocispec.Manifest
179
-			if err := json.Unmarshal(data, &m); err != nil {
180
-				return nil, errors.Wrapf(err, "error unmarshaling image manifest for %s", ref)
181
-			}
182
-			return []ocispec.Descriptor{m.Config}, nil
183
-		case schema2.MediaTypePluginConfig:
184
-			configSeen = true
185
-			data, err := content.ReadBlob(ctx, pm.blobStore, desc)
186
-			if err != nil {
187
-				return nil, errors.Wrapf(err, "error reading plugin config from blob store for %s", ref)
188
-			}
189
-
190
-			if err := json.Unmarshal(data, &config); err != nil {
191
-				return nil, errors.Wrapf(err, "error unmarshaling plugin config for %s", ref)
192
-			}
193
-		}
194
-
195
-		return nil, nil
196
-	}
197
-
198
-	if err := pm.fetch(ctx, ref, authConfig, progress.DiscardOutput(), metaHeader, c8dimages.HandlerFunc(h)); err != nil {
199
-		return types.PluginPrivileges{}, nil
200
-	}
201
-
202
-	if !configSeen {
203
-		return types.PluginPrivileges{}, errors.Errorf("did not find plugin config for specified reference %s", ref)
204
-	}
205
-
206
-	return computePrivileges(config), nil
207
-}
208
-
209
-// Upgrade upgrades a plugin
210
-//
211
-// TODO: replace reference package usage with simpler url.Parse semantics
212
-func (pm *Manager) Upgrade(ctx context.Context, ref reference.Named, name string, metaHeader http.Header, authConfig *registry.AuthConfig, privileges types.PluginPrivileges, outStream io.Writer) error {
213
-	p, err := pm.config.Store.GetV2Plugin(name)
214
-	if err != nil {
215
-		return err
216
-	}
217
-
218
-	if p.IsEnabled() {
219
-		return errors.Wrap(enabledError(p.Name()), "plugin must be disabled before upgrading")
220
-	}
221
-
222
-	// revalidate because Pull is public
223
-	if _, err := reference.ParseNormalizedNamed(name); err != nil {
224
-		return errors.Wrapf(errdefs.InvalidParameter(err), "failed to parse %q", name)
225
-	}
226
-
227
-	pm.muGC.RLock()
228
-	defer pm.muGC.RUnlock()
229
-
230
-	tmpRootFSDir, err := os.MkdirTemp(pm.tmpDir(), ".rootfs")
231
-	if err != nil {
232
-		return errors.Wrap(err, "error creating tmp dir for plugin rootfs")
233
-	}
234
-
235
-	var md fetchMeta
236
-
237
-	ctx, cancel := context.WithCancel(ctx)
238
-	out, waitProgress := setupProgressOutput(outStream, cancel)
239
-	defer waitProgress()
240
-
241
-	if err := pm.fetch(ctx, ref, authConfig, out, metaHeader, storeFetchMetadata(&md), childrenHandler(pm.blobStore), applyLayer(pm.blobStore, tmpRootFSDir, out)); err != nil {
242
-		return err
243
-	}
244
-	pm.config.LogPluginEvent(reference.FamiliarString(ref), name, events.ActionPull)
245
-
246
-	if err := validateFetchedMetadata(md); err != nil {
247
-		return err
248
-	}
249
-
250
-	if err := pm.upgradePlugin(p, md.config, md.manifest, md.blobs, tmpRootFSDir, &privileges); err != nil {
251
-		return err
252
-	}
253
-	p.PluginObj.PluginReference = ref.String()
254
-	return nil
255
-}
256
-
257
-// Pull pulls a plugin, check if the correct privileges are provided and install the plugin.
258
-//
259
-// TODO: replace reference package usage with simpler url.Parse semantics
260
-func (pm *Manager) Pull(ctx context.Context, ref reference.Named, name string, metaHeader http.Header, authConfig *registry.AuthConfig, privileges types.PluginPrivileges, outStream io.Writer, opts ...CreateOpt) error {
261
-	pm.muGC.RLock()
262
-	defer pm.muGC.RUnlock()
263
-
264
-	// revalidate because Pull is public
265
-	nameref, err := reference.ParseNormalizedNamed(name)
266
-	if err != nil {
267
-		return errors.Wrapf(errdefs.InvalidParameter(err), "failed to parse %q", name)
268
-	}
269
-	name = reference.FamiliarString(reference.TagNameOnly(nameref))
270
-
271
-	if err := pm.config.Store.validateName(name); err != nil {
272
-		return errdefs.InvalidParameter(err)
273
-	}
274
-
275
-	tmpRootFSDir, err := os.MkdirTemp(pm.tmpDir(), ".rootfs")
276
-	if err != nil {
277
-		return errors.Wrap(errdefs.System(err), "error preparing upgrade")
278
-	}
279
-	defer os.RemoveAll(tmpRootFSDir)
280
-
281
-	var md fetchMeta
282
-
283
-	ctx, cancel := context.WithCancel(ctx)
284
-	out, waitProgress := setupProgressOutput(outStream, cancel)
285
-	defer waitProgress()
286
-
287
-	if err := pm.fetch(ctx, ref, authConfig, out, metaHeader, storeFetchMetadata(&md), childrenHandler(pm.blobStore), applyLayer(pm.blobStore, tmpRootFSDir, out)); err != nil {
288
-		return err
289
-	}
290
-	pm.config.LogPluginEvent(reference.FamiliarString(ref), name, events.ActionPull)
291
-
292
-	if err := validateFetchedMetadata(md); err != nil {
293
-		return err
294
-	}
295
-
296
-	refOpt := func(p *v2.Plugin) {
297
-		p.PluginObj.PluginReference = ref.String()
298
-	}
299
-	optsList := make([]CreateOpt, 0, len(opts)+1)
300
-	optsList = append(optsList, opts...)
301
-	optsList = append(optsList, refOpt)
302
-
303
-	// TODO: tmpRootFSDir is empty but should have layers in it
304
-	p, err := pm.createPlugin(name, md.config, md.manifest, md.blobs, tmpRootFSDir, &privileges, optsList...)
305
-	if err != nil {
306
-		return err
307
-	}
308
-
309
-	pm.publisher.Publish(EventCreate{Plugin: p.PluginObj})
310
-
311
-	return nil
312
-}
313
-
314
-// List displays the list of plugins and associated metadata.
315
-func (pm *Manager) List(pluginFilters filters.Args) ([]types.Plugin, error) {
316
-	if err := pluginFilters.Validate(acceptedPluginFilterTags); err != nil {
317
-		return nil, err
318
-	}
319
-
320
-	enabledOnly := false
321
-	disabledOnly := false
322
-	if pluginFilters.Contains("enabled") {
323
-		enabledFilter, err := pluginFilters.GetBoolOrDefault("enabled", false)
324
-		if err != nil {
325
-			return nil, err
326
-		}
327
-
328
-		if enabledFilter {
329
-			enabledOnly = true
330
-		} else {
331
-			disabledOnly = true
332
-		}
333
-	}
334
-
335
-	plugins := pm.config.Store.GetAll()
336
-	out := make([]types.Plugin, 0, len(plugins))
337
-
338
-next:
339
-	for _, p := range plugins {
340
-		if enabledOnly && !p.PluginObj.Enabled {
341
-			continue
342
-		}
343
-		if disabledOnly && p.PluginObj.Enabled {
344
-			continue
345
-		}
346
-		if pluginFilters.Contains("capability") {
347
-			for _, f := range p.GetTypes() {
348
-				if !pluginFilters.Match("capability", f.Capability) {
349
-					continue next
350
-				}
351
-			}
352
-		}
353
-		out = append(out, p.PluginObj)
354
-	}
355
-	return out, nil
356
-}
357
-
358
-// Push pushes a plugin to the registry.
359
-func (pm *Manager) Push(ctx context.Context, name string, metaHeader http.Header, authConfig *registry.AuthConfig, outStream io.Writer) error {
360
-	p, err := pm.config.Store.GetV2Plugin(name)
361
-	if err != nil {
362
-		return err
363
-	}
364
-
365
-	ref, err := reference.ParseNormalizedNamed(p.Name())
366
-	if err != nil {
367
-		return errors.Wrapf(err, "plugin has invalid name %v for push", p.Name())
368
-	}
369
-
370
-	statusTracker := docker.NewInMemoryTracker()
371
-
372
-	resolver, err := pm.newResolver(ctx, statusTracker, authConfig, metaHeader, false)
373
-	if err != nil {
374
-		return err
375
-	}
376
-
377
-	pusher, err := resolver.Pusher(ctx, ref.String())
378
-	if err != nil {
379
-		return errors.Wrap(err, "error creating plugin pusher")
380
-	}
381
-
382
-	pj := newPushJobs(statusTracker)
383
-
384
-	ctx, cancel := context.WithCancel(ctx)
385
-	out, waitProgress := setupProgressOutput(outStream, cancel)
386
-	defer waitProgress()
387
-
388
-	progressHandler := c8dimages.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
389
-		log.G(ctx).WithField("mediaType", desc.MediaType).WithField("digest", desc.Digest.String()).Debug("Preparing to push plugin layer")
390
-		id := stringid.TruncateID(desc.Digest.String())
391
-		pj.add(remotes.MakeRefKey(ctx, desc), id)
392
-		progress.Update(out, id, "Preparing")
393
-		return nil, nil
394
-	})
395
-
396
-	desc, err := pm.getManifestDescriptor(ctx, p)
397
-	if err != nil {
398
-		return errors.Wrap(err, "error reading plugin manifest")
399
-	}
400
-
401
-	progress.Messagef(out, "", "The push refers to repository [%s]", reference.FamiliarName(ref))
402
-
403
-	// TODO: If a layer already exists on the registry, the progress output just says "Preparing"
404
-	go func() {
405
-		timer := time.NewTimer(100 * time.Millisecond)
406
-		defer timer.Stop()
407
-		if !timer.Stop() {
408
-			<-timer.C
409
-		}
410
-		var statuses []contentStatus
411
-		for {
412
-			timer.Reset(100 * time.Millisecond)
413
-			select {
414
-			case <-ctx.Done():
415
-				return
416
-			case <-timer.C:
417
-				statuses = pj.status()
418
-			}
419
-
420
-			for _, s := range statuses {
421
-				out.WriteProgress(progress.Progress{ID: s.Ref, Current: s.Offset, Total: s.Total, Action: s.Status, LastUpdate: s.Offset == s.Total})
422
-			}
423
-		}
424
-	}()
425
-
426
-	// Make sure we can authenticate the request since the auth scope for plugin repos is different than a normal repo.
427
-	ctx = docker.WithScope(ctx, scope(ref, true))
428
-	if err := remotes.PushContent(ctx, pusher, desc, pm.blobStore, nil, nil, func(h c8dimages.Handler) c8dimages.Handler {
429
-		return c8dimages.Handlers(progressHandler, h)
430
-	}); err != nil {
431
-		// Try fallback to http.
432
-		// This is needed because the containerd pusher will only attempt the first registry config we pass, which would
433
-		// typically be https.
434
-		// If there are no http-only host configs found we'll error out anyway.
435
-		resolver, _ := pm.newResolver(ctx, statusTracker, authConfig, metaHeader, true)
436
-		if resolver != nil {
437
-			pusher, _ := resolver.Pusher(ctx, ref.String())
438
-			if pusher != nil {
439
-				log.G(ctx).WithField("ref", ref).Debug("Re-attmpting push with http-fallback")
440
-				err2 := remotes.PushContent(ctx, pusher, desc, pm.blobStore, nil, nil, func(h c8dimages.Handler) c8dimages.Handler {
441
-					return c8dimages.Handlers(progressHandler, h)
442
-				})
443
-				if err2 == nil {
444
-					err = nil
445
-				} else {
446
-					log.G(ctx).WithError(err2).WithField("ref", ref).Debug("Error while attempting push with http-fallback")
447
-				}
448
-			}
449
-		}
450
-		if err != nil {
451
-			return errors.Wrap(err, "error pushing plugin")
452
-		}
453
-	}
454
-
455
-	// For blobs that already exist in the registry we need to make sure to update the progress otherwise it will just say "pending"
456
-	// TODO: How to check if the layer already exists? Is it worth it?
457
-	for _, j := range pj.jobs {
458
-		progress.Update(out, pj.names[j], "Upload complete")
459
-	}
460
-
461
-	// Signal the client for content trust verification
462
-	progress.Aux(out, types.PushResult{Tag: ref.(reference.Tagged).Tag(), Digest: desc.Digest.String(), Size: int(desc.Size)})
463
-
464
-	return nil
465
-}
466
-
467
-// manifest wraps an OCI manifest, because...
468
-// Historically the registry does not support plugins unless the media type on the manifest is specifically schema2.MediaTypeManifest
469
-// So the OCI manifest media type is not supported.
470
-// Additionally, there is extra validation for the docker schema2 manifest than there is a mediatype set on the manifest itself
471
-// even though this is set on the descriptor
472
-// The OCI types do not have this field.
473
-type manifest struct {
474
-	ocispec.Manifest
475
-	MediaType string `json:"mediaType,omitempty"`
476
-}
477
-
478
-func buildManifest(ctx context.Context, s content.Manager, config digest.Digest, layers []digest.Digest) (manifest, error) {
479
-	var m manifest
480
-	m.MediaType = c8dimages.MediaTypeDockerSchema2Manifest
481
-	m.SchemaVersion = 2
482
-
483
-	configInfo, err := s.Info(ctx, config)
484
-	if err != nil {
485
-		return m, errors.Wrapf(err, "error reading plugin config content for digest %s", config)
486
-	}
487
-	m.Config = ocispec.Descriptor{
488
-		MediaType: mediaTypePluginConfig,
489
-		Size:      configInfo.Size,
490
-		Digest:    configInfo.Digest,
491
-	}
492
-
493
-	for _, l := range layers {
494
-		info, err := s.Info(ctx, l)
495
-		if err != nil {
496
-			return m, errors.Wrapf(err, "error fetching info for content digest %s", l)
497
-		}
498
-		m.Layers = append(m.Layers, ocispec.Descriptor{
499
-			MediaType: c8dimages.MediaTypeDockerSchema2LayerGzip, // TODO: This is assuming everything is a gzip compressed layer, but that may not be true.
500
-			Digest:    l,
501
-			Size:      info.Size,
502
-		})
503
-	}
504
-	return m, nil
505
-}
506
-
507
-// getManifestDescriptor gets the OCI descriptor for a manifest
508
-// It will generate a manifest if one does not exist
509
-func (pm *Manager) getManifestDescriptor(ctx context.Context, p *v2.Plugin) (ocispec.Descriptor, error) {
510
-	logger := log.G(ctx).WithField("plugin", p.Name()).WithField("digest", p.Manifest)
511
-	if p.Manifest != "" {
512
-		info, err := pm.blobStore.Info(ctx, p.Manifest)
513
-		if err == nil {
514
-			desc := ocispec.Descriptor{
515
-				Size:      info.Size,
516
-				Digest:    info.Digest,
517
-				MediaType: c8dimages.MediaTypeDockerSchema2Manifest,
518
-			}
519
-			return desc, nil
520
-		}
521
-		logger.WithError(err).Debug("Could not find plugin manifest in content store")
522
-	} else {
523
-		logger.Info("Plugin does not have manifest digest")
524
-	}
525
-	logger.Info("Building a new plugin manifest")
526
-
527
-	mfst, err := buildManifest(ctx, pm.blobStore, p.Config, p.Blobsums)
528
-	if err != nil {
529
-		return ocispec.Descriptor{}, err
530
-	}
531
-
532
-	desc, err := writeManifest(ctx, pm.blobStore, &mfst)
533
-	if err != nil {
534
-		return desc, err
535
-	}
536
-
537
-	if err := pm.save(p); err != nil {
538
-		logger.WithError(err).Error("Could not save plugin with manifest digest")
539
-	}
540
-	return desc, nil
541
-}
542
-
543
-func writeManifest(ctx context.Context, cs content.Store, m *manifest) (ocispec.Descriptor, error) {
544
-	platform := platforms.DefaultSpec()
545
-	desc := ocispec.Descriptor{
546
-		MediaType: c8dimages.MediaTypeDockerSchema2Manifest,
547
-		Platform:  &platform,
548
-	}
549
-	data, err := json.Marshal(m)
550
-	if err != nil {
551
-		return desc, errors.Wrap(err, "error encoding manifest")
552
-	}
553
-	desc.Digest = digest.FromBytes(data)
554
-	desc.Size = int64(len(data))
555
-
556
-	if err := content.WriteBlob(ctx, cs, remotes.MakeRefKey(ctx, desc), bytes.NewReader(data), desc); err != nil {
557
-		return desc, errors.Wrap(err, "error writing plugin manifest")
558
-	}
559
-	return desc, nil
560
-}
561
-
562
-// Remove deletes plugin's root directory.
563
-func (pm *Manager) Remove(name string, config *backend.PluginRmConfig) error {
564
-	p, err := pm.config.Store.GetV2Plugin(name)
565
-	pm.mu.RLock()
566
-	c := pm.cMap[p]
567
-	pm.mu.RUnlock()
568
-
569
-	if err != nil {
570
-		return err
571
-	}
572
-
573
-	if !config.ForceRemove {
574
-		if p.GetRefCount() > 0 {
575
-			return inUseError(p.Name())
576
-		}
577
-		if p.IsEnabled() {
578
-			return enabledError(p.Name())
579
-		}
580
-	}
581
-
582
-	if p.IsEnabled() {
583
-		if err := pm.disable(p, c); err != nil {
584
-			log.G(context.TODO()).Errorf("failed to disable plugin '%s': %s", p.Name(), err)
585
-		}
586
-	}
587
-
588
-	defer func() {
589
-		go pm.GC()
590
-	}()
591
-
592
-	id := p.GetID()
593
-	pluginDir := filepath.Join(pm.config.Root, id)
594
-
595
-	if err := mount.RecursiveUnmount(pluginDir); err != nil {
596
-		return errors.Wrap(err, "error unmounting plugin data")
597
-	}
598
-
599
-	if err := atomicRemoveAll(pluginDir); err != nil {
600
-		return err
601
-	}
602
-
603
-	pm.config.Store.Remove(p)
604
-	pm.config.LogPluginEvent(id, name, events.ActionRemove)
605
-	pm.publisher.Publish(EventRemove{Plugin: p.PluginObj})
606
-	return nil
607
-}
608
-
609
-// Set sets plugin args
610
-func (pm *Manager) Set(name string, args []string) error {
611
-	p, err := pm.config.Store.GetV2Plugin(name)
612
-	if err != nil {
613
-		return err
614
-	}
615
-	if err := p.Set(args); err != nil {
616
-		return err
617
-	}
618
-	return pm.save(p)
619
-}
620
-
621
-// CreateFromContext creates a plugin from the given pluginDir which contains
622
-// both the rootfs and the config.json and a repoName with optional tag.
623
-func (pm *Manager) CreateFromContext(ctx context.Context, tarCtx io.ReadCloser, options *types.PluginCreateOptions) (retErr error) {
624
-	pm.muGC.RLock()
625
-	defer pm.muGC.RUnlock()
626
-
627
-	ref, err := reference.ParseNormalizedNamed(options.RepoName)
628
-	if err != nil {
629
-		return errors.Wrapf(err, "failed to parse reference %v", options.RepoName)
630
-	}
631
-	if _, ok := ref.(reference.Canonical); ok {
632
-		return errors.Errorf("canonical references are not permitted")
633
-	}
634
-	name := reference.FamiliarString(reference.TagNameOnly(ref))
635
-
636
-	if err := pm.config.Store.validateName(name); err != nil { // fast check, real check is in createPlugin()
637
-		return err
638
-	}
639
-
640
-	tmpRootFSDir, err := os.MkdirTemp(pm.tmpDir(), ".rootfs")
641
-	if err != nil {
642
-		return errors.Wrap(err, "failed to create temp directory")
643
-	}
644
-	defer func() {
645
-		if err := os.RemoveAll(tmpRootFSDir); err != nil {
646
-			log.G(ctx).WithError(err).Warn("failed to remove temp rootfs directory")
647
-		}
648
-	}()
649
-
650
-	var configJSON []byte
651
-	rootFS := splitConfigRootFSFromTar(tarCtx, &configJSON)
652
-
653
-	rootFSBlob, err := pm.blobStore.Writer(ctx, content.WithRef(name))
654
-	if err != nil {
655
-		return err
656
-	}
657
-	defer rootFSBlob.Close()
658
-
659
-	gzw := gzip.NewWriter(rootFSBlob)
660
-	rootFSReader := io.TeeReader(rootFS, gzw)
661
-
662
-	if err := chrootarchive.Untar(rootFSReader, tmpRootFSDir, nil); err != nil {
663
-		return err
664
-	}
665
-	if err := rootFS.Close(); err != nil {
666
-		return err
667
-	}
668
-
669
-	if configJSON == nil {
670
-		return errors.New("config not found")
671
-	}
672
-
673
-	if err := gzw.Close(); err != nil {
674
-		return errors.Wrap(err, "error closing gzip writer")
675
-	}
676
-
677
-	var config types.PluginConfig
678
-	if err := json.Unmarshal(configJSON, &config); err != nil {
679
-		return errors.Wrap(err, "failed to parse config")
680
-	}
681
-
682
-	if err := pm.validateConfig(config); err != nil {
683
-		return err
684
-	}
685
-
686
-	pm.mu.Lock()
687
-	defer pm.mu.Unlock()
688
-
689
-	if err := rootFSBlob.Commit(ctx, 0, ""); err != nil {
690
-		return err
691
-	}
692
-	defer func() {
693
-		if retErr != nil {
694
-			go pm.GC()
695
-		}
696
-	}()
697
-
698
-	config.Rootfs = &types.PluginConfigRootfs{
699
-		Type:    "layers",
700
-		DiffIds: []string{rootFSBlob.Digest().String()},
701
-	}
702
-
703
-	config.DockerVersion = dockerversion.Version
704
-
705
-	configBlob, err := pm.blobStore.Writer(ctx, content.WithRef(name+"-config.json"))
706
-	if err != nil {
707
-		return err
708
-	}
709
-	defer configBlob.Close()
710
-	if err := json.NewEncoder(configBlob).Encode(config); err != nil {
711
-		return errors.Wrap(err, "error encoding json config")
712
-	}
713
-	if err := configBlob.Commit(ctx, 0, ""); err != nil {
714
-		return err
715
-	}
716
-
717
-	configDigest := configBlob.Digest()
718
-	layers := []digest.Digest{rootFSBlob.Digest()}
719
-
720
-	mfst, err := buildManifest(ctx, pm.blobStore, configDigest, layers)
721
-	if err != nil {
722
-		return err
723
-	}
724
-	desc, err := writeManifest(ctx, pm.blobStore, &mfst)
725
-	if err != nil {
726
-		return err
727
-	}
728
-
729
-	p, err := pm.createPlugin(name, configDigest, desc.Digest, layers, tmpRootFSDir, nil)
730
-	if err != nil {
731
-		return err
732
-	}
733
-	p.PluginObj.PluginReference = name
734
-
735
-	pm.publisher.Publish(EventCreate{Plugin: p.PluginObj})
736
-	pm.config.LogPluginEvent(p.PluginObj.ID, name, events.ActionCreate)
737
-
738
-	return nil
739
-}
740
-
741
-func (pm *Manager) validateConfig(config types.PluginConfig) error {
742
-	return nil // TODO:
743
-}
744
-
745
-func splitConfigRootFSFromTar(in io.ReadCloser, config *[]byte) io.ReadCloser {
746
-	pr, pw := io.Pipe()
747
-	go func() {
748
-		tarReader := tar.NewReader(in)
749
-		tarWriter := tar.NewWriter(pw)
750
-		defer in.Close()
751
-
752
-		hasRootFS := false
753
-
754
-		for {
755
-			hdr, err := tarReader.Next()
756
-			if err == io.EOF {
757
-				if !hasRootFS {
758
-					pw.CloseWithError(errors.Wrap(err, "no rootfs found"))
759
-					return
760
-				}
761
-				// Signals end of archive.
762
-				tarWriter.Close()
763
-				pw.Close()
764
-				return
765
-			}
766
-			if err != nil {
767
-				pw.CloseWithError(errors.Wrap(err, "failed to read from tar"))
768
-				return
769
-			}
770
-
771
-			content := io.Reader(tarReader)
772
-			name := path.Clean(hdr.Name)
773
-			if path.IsAbs(name) {
774
-				name = name[1:]
775
-			}
776
-			if name == configFileName {
777
-				dt, err := io.ReadAll(content)
778
-				if err != nil {
779
-					pw.CloseWithError(errors.Wrapf(err, "failed to read %s", configFileName))
780
-					return
781
-				}
782
-				*config = dt
783
-			}
784
-			if parts := strings.Split(name, "/"); len(parts) != 0 && parts[0] == rootFSFileName {
785
-				hdr.Name = path.Clean(path.Join(parts[1:]...))
786
-				if hdr.Typeflag == tar.TypeLink && strings.HasPrefix(strings.ToLower(hdr.Linkname), rootFSFileName+"/") {
787
-					hdr.Linkname = hdr.Linkname[len(rootFSFileName)+1:]
788
-				}
789
-				if err := tarWriter.WriteHeader(hdr); err != nil {
790
-					pw.CloseWithError(errors.Wrap(err, "error writing tar header"))
791
-					return
792
-				}
793
-				if _, err := pools.Copy(tarWriter, content); err != nil {
794
-					pw.CloseWithError(errors.Wrap(err, "error copying tar data"))
795
-					return
796
-				}
797
-				hasRootFS = true
798
-			} else {
799
-				io.Copy(io.Discard, content)
800
-			}
801
-		}
802
-	}()
803
-	return pr
804
-}
805
-
806
-func atomicRemoveAll(dir string) error {
807
-	renamed := dir + "-removing"
808
-
809
-	err := os.Rename(dir, renamed)
810
-	switch {
811
-	case os.IsNotExist(err), err == nil:
812
-		// even if `dir` doesn't exist, we can still try and remove `renamed`
813
-	case os.IsExist(err):
814
-		// Some previous remove failed, check if the origin dir exists
815
-		if e := containerfs.EnsureRemoveAll(renamed); e != nil {
816
-			return errors.Wrap(err, "rename target already exists and could not be removed")
817
-		}
818
-		if _, err := os.Stat(dir); os.IsNotExist(err) {
819
-			// origin doesn't exist, nothing left to do
820
-			return nil
821
-		}
822
-
823
-		// attempt to rename again
824
-		if err := os.Rename(dir, renamed); err != nil {
825
-			return errors.Wrap(err, "failed to rename dir for atomic removal")
826
-		}
827
-	default:
828
-		return errors.Wrap(err, "failed to rename dir for atomic removal")
829
-	}
830
-
831
-	if err := containerfs.EnsureRemoveAll(renamed); err != nil {
832
-		os.Rename(renamed, dir)
833
-		return err
834
-	}
835
-	return nil
836
-}
837 1
deleted file mode 100644
... ...
@@ -1,68 +0,0 @@
1
-package plugin
2
-
3
-import (
4
-	"os"
5
-	"path/filepath"
6
-	"testing"
7
-)
8
-
9
-func TestAtomicRemoveAllNormal(t *testing.T) {
10
-	dir := t.TempDir()
11
-
12
-	if err := atomicRemoveAll(dir); err != nil {
13
-		t.Fatal(err)
14
-	}
15
-
16
-	if _, err := os.Stat(dir); !os.IsNotExist(err) {
17
-		t.Fatalf("dir should be gone: %v", err)
18
-	}
19
-	if _, err := os.Stat(dir + "-removing"); !os.IsNotExist(err) {
20
-		t.Fatalf("dir should be gone: %v", err)
21
-	}
22
-}
23
-
24
-func TestAtomicRemoveAllAlreadyExists(t *testing.T) {
25
-	dir := t.TempDir()
26
-
27
-	if err := os.MkdirAll(dir+"-removing", 0o755); err != nil {
28
-		t.Fatal(err)
29
-	}
30
-	defer os.RemoveAll(dir + "-removing")
31
-
32
-	if err := atomicRemoveAll(dir); err != nil {
33
-		t.Fatal(err)
34
-	}
35
-
36
-	if _, err := os.Stat(dir); !os.IsNotExist(err) {
37
-		t.Fatalf("dir should be gone: %v", err)
38
-	}
39
-	if _, err := os.Stat(dir + "-removing"); !os.IsNotExist(err) {
40
-		t.Fatalf("dir should be gone: %v", err)
41
-	}
42
-}
43
-
44
-func TestAtomicRemoveAllNotExist(t *testing.T) {
45
-	if err := atomicRemoveAll("/not-exist"); err != nil {
46
-		t.Fatal(err)
47
-	}
48
-
49
-	dir := t.TempDir()
50
-
51
-	// create the removing dir, but not the "real" one
52
-	foo := filepath.Join(dir, "foo")
53
-	removing := dir + "-removing"
54
-	if err := os.MkdirAll(removing, 0o755); err != nil {
55
-		t.Fatal(err)
56
-	}
57
-
58
-	if err := atomicRemoveAll(dir); err != nil {
59
-		t.Fatal(err)
60
-	}
61
-
62
-	if _, err := os.Stat(foo); !os.IsNotExist(err) {
63
-		t.Fatalf("dir should be gone: %v", err)
64
-	}
65
-	if _, err := os.Stat(removing); !os.IsNotExist(err) {
66
-		t.Fatalf("dir should be gone: %v", err)
67
-	}
68
-}
69 1
deleted file mode 100644
... ...
@@ -1,74 +0,0 @@
1
-//go:build !linux
2
-
3
-package plugin
4
-
5
-import (
6
-	"context"
7
-	"errors"
8
-	"io"
9
-	"net/http"
10
-
11
-	"github.com/distribution/reference"
12
-	"github.com/docker/docker/api/types"
13
-	"github.com/docker/docker/api/types/backend"
14
-	"github.com/docker/docker/api/types/filters"
15
-	"github.com/docker/docker/api/types/registry"
16
-)
17
-
18
-var errNotSupported = errors.New("plugins are not supported on this platform")
19
-
20
-// Disable deactivates a plugin, which implies that they cannot be used by containers.
21
-func (pm *Manager) Disable(name string, config *backend.PluginDisableConfig) error {
22
-	return errNotSupported
23
-}
24
-
25
-// Enable activates a plugin, which implies that they are ready to be used by containers.
26
-func (pm *Manager) Enable(name string, config *backend.PluginEnableConfig) error {
27
-	return errNotSupported
28
-}
29
-
30
-// Inspect examines a plugin config
31
-func (pm *Manager) Inspect(refOrID string) (*types.Plugin, error) {
32
-	return nil, errNotSupported
33
-}
34
-
35
-// Privileges pulls a plugin config and computes the privileges required to install it.
36
-func (pm *Manager) Privileges(ctx context.Context, ref reference.Named, metaHeader http.Header, authConfig *registry.AuthConfig) (types.PluginPrivileges, error) {
37
-	return nil, errNotSupported
38
-}
39
-
40
-// Pull pulls a plugin, check if the correct privileges are provided and install the plugin.
41
-func (pm *Manager) Pull(ctx context.Context, ref reference.Named, name string, metaHeader http.Header, authConfig *registry.AuthConfig, privileges types.PluginPrivileges, out io.Writer, opts ...CreateOpt) error {
42
-	return errNotSupported
43
-}
44
-
45
-// Upgrade pulls a plugin, check if the correct privileges are provided and install the plugin.
46
-func (pm *Manager) Upgrade(ctx context.Context, ref reference.Named, name string, metaHeader http.Header, authConfig *registry.AuthConfig, privileges types.PluginPrivileges, outStream io.Writer) error {
47
-	return errNotSupported
48
-}
49
-
50
-// List displays the list of plugins and associated metadata.
51
-func (pm *Manager) List(pluginFilters filters.Args) ([]types.Plugin, error) {
52
-	return nil, errNotSupported
53
-}
54
-
55
-// Push pushes a plugin to the store.
56
-func (pm *Manager) Push(ctx context.Context, name string, metaHeader http.Header, authConfig *registry.AuthConfig, out io.Writer) error {
57
-	return errNotSupported
58
-}
59
-
60
-// Remove deletes plugin's root directory.
61
-func (pm *Manager) Remove(name string, config *backend.PluginRmConfig) error {
62
-	return errNotSupported
63
-}
64
-
65
-// Set sets plugin args
66
-func (pm *Manager) Set(name string, args []string) error {
67
-	return errNotSupported
68
-}
69
-
70
-// CreateFromContext creates a plugin from the given pluginDir which contains
71
-// both the rootfs and the config.json and a repoName with optional tag.
72
-func (pm *Manager) CreateFromContext(ctx context.Context, tarCtx io.ReadCloser, options *types.PluginCreateOptions) error {
73
-	return errNotSupported
74
-}
75 1
deleted file mode 100644
... ...
@@ -1,74 +0,0 @@
1
-package plugin
2
-
3
-import (
4
-	"strings"
5
-	"sync"
6
-
7
-	v2 "github.com/docker/docker/daemon/pkg/plugin/v2"
8
-	"github.com/docker/docker/pkg/plugins"
9
-	"github.com/opencontainers/runtime-spec/specs-go"
10
-)
11
-
12
-// Store manages the plugin inventory in memory and on-disk
13
-type Store struct {
14
-	sync.RWMutex
15
-	plugins  map[string]*v2.Plugin
16
-	specOpts map[string][]SpecOpt
17
-	/* handlers are necessary for transition path of legacy plugins
18
-	 * to the new model. Legacy plugins use Handle() for registering an
19
-	 * activation callback.*/
20
-	handlers map[string][]func(string, *plugins.Client)
21
-}
22
-
23
-// NewStore creates a Store.
24
-func NewStore() *Store {
25
-	return &Store{
26
-		plugins:  make(map[string]*v2.Plugin),
27
-		specOpts: make(map[string][]SpecOpt),
28
-		handlers: make(map[string][]func(string, *plugins.Client)),
29
-	}
30
-}
31
-
32
-// SpecOpt is used for subsystems that need to modify the runtime spec of a plugin
33
-type SpecOpt func(*specs.Spec)
34
-
35
-// CreateOpt is used to configure specific plugin details when created
36
-type CreateOpt func(p *v2.Plugin)
37
-
38
-// WithSwarmService is a CreateOpt that flags the passed in a plugin as a plugin
39
-// managed by swarm
40
-func WithSwarmService(id string) CreateOpt {
41
-	return func(p *v2.Plugin) {
42
-		p.SwarmServiceID = id
43
-	}
44
-}
45
-
46
-// WithEnv is a CreateOpt that passes the user-provided environment variables
47
-// to the plugin container, de-duplicating variables with the same names case
48
-// sensitively and only appends valid key=value pairs
49
-func WithEnv(env []string) CreateOpt {
50
-	return func(p *v2.Plugin) {
51
-		effectiveEnv := make(map[string]string)
52
-		for _, penv := range p.PluginObj.Config.Env {
53
-			if penv.Value != nil {
54
-				effectiveEnv[penv.Name] = *penv.Value
55
-			}
56
-		}
57
-		for _, line := range env {
58
-			if k, v, ok := strings.Cut(line, "="); ok {
59
-				effectiveEnv[k] = v
60
-			}
61
-		}
62
-		p.PluginObj.Settings.Env = make([]string, 0, len(effectiveEnv))
63
-		for key, value := range effectiveEnv {
64
-			p.PluginObj.Settings.Env = append(p.PluginObj.Settings.Env, key+"="+value)
65
-		}
66
-	}
67
-}
68
-
69
-// WithSpecMounts is a SpecOpt which appends the provided mounts to the runtime spec
70
-func WithSpecMounts(mounts []specs.Mount) SpecOpt {
71
-	return func(s *specs.Spec) {
72
-		s.Mounts = append(s.Mounts, mounts...)
73
-	}
74
-}
75 1
deleted file mode 100644
... ...
@@ -1,51 +0,0 @@
1
-package plugin
2
-
3
-import "fmt"
4
-
5
-type errNotFound string
6
-
7
-func (name errNotFound) Error() string {
8
-	return fmt.Sprintf("plugin %q not found", string(name))
9
-}
10
-
11
-func (errNotFound) NotFound() {}
12
-
13
-type errAmbiguous string
14
-
15
-func (name errAmbiguous) Error() string {
16
-	return fmt.Sprintf("multiple plugins found for %q", string(name))
17
-}
18
-
19
-func (name errAmbiguous) InvalidParameter() {}
20
-
21
-type errDisabled string
22
-
23
-func (name errDisabled) Error() string {
24
-	return fmt.Sprintf("plugin %s found but disabled", string(name))
25
-}
26
-
27
-func (name errDisabled) Conflict() {}
28
-
29
-type inUseError string
30
-
31
-func (e inUseError) Error() string {
32
-	return "plugin " + string(e) + " is in use"
33
-}
34
-
35
-func (inUseError) Conflict() {}
36
-
37
-type enabledError string
38
-
39
-func (e enabledError) Error() string {
40
-	return "plugin " + string(e) + " is enabled"
41
-}
42
-
43
-func (enabledError) Conflict() {}
44
-
45
-type alreadyExistsError string
46
-
47
-func (e alreadyExistsError) Error() string {
48
-	return "plugin " + string(e) + " already exists"
49
-}
50
-
51
-func (alreadyExistsError) Conflict() {}
52 1
deleted file mode 100644
... ...
@@ -1,111 +0,0 @@
1
-package plugin
2
-
3
-import (
4
-	"fmt"
5
-	"reflect"
6
-
7
-	"github.com/docker/docker/api/types"
8
-)
9
-
10
-// Event is emitted for actions performed on the plugin manager
11
-type Event interface {
12
-	matches(Event) bool
13
-}
14
-
15
-// EventCreate is an event which is emitted when a plugin is created
16
-// This is either by pull or create from context.
17
-//
18
-// Use the `Interfaces` field to match only plugins that implement a specific
19
-// interface.
20
-// These are matched against using "or" logic.
21
-// If no interfaces are listed, all are matched.
22
-type EventCreate struct {
23
-	Interfaces map[string]bool
24
-	Plugin     types.Plugin
25
-}
26
-
27
-func (e EventCreate) matches(observed Event) bool {
28
-	oe, ok := observed.(EventCreate)
29
-	if !ok {
30
-		return false
31
-	}
32
-	if len(e.Interfaces) == 0 {
33
-		return true
34
-	}
35
-
36
-	var ifaceMatch bool
37
-	for _, in := range oe.Plugin.Config.Interface.Types {
38
-		if e.Interfaces[in.Capability] {
39
-			ifaceMatch = true
40
-			break
41
-		}
42
-	}
43
-	return ifaceMatch
44
-}
45
-
46
-// EventRemove is an event which is emitted when a plugin is removed
47
-// It matches on the passed in plugin's ID only.
48
-type EventRemove struct {
49
-	Plugin types.Plugin
50
-}
51
-
52
-func (e EventRemove) matches(observed Event) bool {
53
-	oe, ok := observed.(EventRemove)
54
-	if !ok {
55
-		return false
56
-	}
57
-	return e.Plugin.ID == oe.Plugin.ID
58
-}
59
-
60
-// EventDisable is an event that is emitted when a plugin is disabled
61
-// It matches on the passed in plugin's ID only.
62
-type EventDisable struct {
63
-	Plugin types.Plugin
64
-}
65
-
66
-func (e EventDisable) matches(observed Event) bool {
67
-	oe, ok := observed.(EventDisable)
68
-	if !ok {
69
-		return false
70
-	}
71
-	return e.Plugin.ID == oe.Plugin.ID
72
-}
73
-
74
-// EventEnable is an event that is emitted when a plugin is disabled
75
-// It matches on the passed in plugin's ID only.
76
-type EventEnable struct {
77
-	Plugin types.Plugin
78
-}
79
-
80
-func (e EventEnable) matches(observed Event) bool {
81
-	oe, ok := observed.(EventEnable)
82
-	if !ok {
83
-		return false
84
-	}
85
-	return e.Plugin.ID == oe.Plugin.ID
86
-}
87
-
88
-// SubscribeEvents provides an event channel to listen for structured events from
89
-// the plugin manager actions, CRUD operations.
90
-// The caller must call the returned `cancel()` function once done with the channel
91
-// or this will leak resources.
92
-func (pm *Manager) SubscribeEvents(buffer int, watchEvents ...Event) (eventCh <-chan interface{}, cancel func()) {
93
-	topic := func(i interface{}) bool {
94
-		observed, ok := i.(Event)
95
-		if !ok {
96
-			panic(fmt.Sprintf("unexpected type passed to event channel: %v", reflect.TypeOf(i)))
97
-		}
98
-		for _, e := range watchEvents {
99
-			if e.matches(observed) {
100
-				return true
101
-			}
102
-		}
103
-		// If no specific events are specified always assume a matched event
104
-		// If some events were specified and none matched above, then the event
105
-		// doesn't match
106
-		return watchEvents == nil
107
-	}
108
-	ch := pm.publisher.SubscribeTopicWithBuffer(topic, buffer)
109
-	cancelFunc := func() { pm.publisher.Evict(ch) }
110
-	return ch, cancelFunc
111
-}
112 1
deleted file mode 100644
... ...
@@ -1,293 +0,0 @@
1
-package plugin
2
-
3
-import (
4
-	"context"
5
-	"io"
6
-	"net/http"
7
-	"time"
8
-
9
-	"github.com/containerd/containerd/v2/core/content"
10
-	c8dimages "github.com/containerd/containerd/v2/core/images"
11
-	"github.com/containerd/containerd/v2/core/remotes"
12
-	"github.com/containerd/containerd/v2/core/remotes/docker"
13
-	cerrdefs "github.com/containerd/errdefs"
14
-	"github.com/containerd/log"
15
-	"github.com/distribution/reference"
16
-	"github.com/docker/docker/api/types/registry"
17
-	progressutils "github.com/docker/docker/distribution/utils"
18
-	"github.com/docker/docker/pkg/ioutils"
19
-	"github.com/docker/docker/pkg/progress"
20
-	"github.com/docker/docker/pkg/stringid"
21
-	"github.com/moby/go-archive/chrootarchive"
22
-	"github.com/opencontainers/go-digest"
23
-	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
24
-	"github.com/pkg/errors"
25
-)
26
-
27
-const mediaTypePluginConfig = "application/vnd.docker.plugin.v1+json"
28
-
29
-// setupProgressOutput sets up the passed in writer to stream progress.
30
-//
31
-// The passed in cancel function is used by the progress writer to signal callers that there
32
-// is an issue writing to the stream.
33
-//
34
-// The returned function is used to wait for the progress writer to be finished.
35
-// Call it to make sure the progress writer is done before returning from your function as needed.
36
-func setupProgressOutput(outStream io.Writer, cancel func()) (progress.Output, func()) {
37
-	var out progress.Output
38
-	f := func() {}
39
-
40
-	if outStream != nil {
41
-		ch := make(chan progress.Progress, 100)
42
-		out = progress.ChanOutput(ch)
43
-
44
-		ctx, retCancel := context.WithCancel(context.Background())
45
-		go func() {
46
-			progressutils.WriteDistributionProgress(cancel, outStream, ch)
47
-			retCancel()
48
-		}()
49
-
50
-		f = func() {
51
-			close(ch)
52
-			<-ctx.Done()
53
-		}
54
-	} else {
55
-		out = progress.DiscardOutput()
56
-	}
57
-	return out, f
58
-}
59
-
60
-// fetch the content related to the passed in reference into the blob store and appends the provided c8dimages.Handlers
61
-// There is no need to use remotes.FetchHandler since it already gets set
62
-func (pm *Manager) fetch(ctx context.Context, ref reference.Named, auth *registry.AuthConfig, out progress.Output, metaHeader http.Header, handlers ...c8dimages.Handler) error {
63
-	// We need to make sure we have a domain on the reference
64
-	withDomain, err := reference.ParseNormalizedNamed(ref.String())
65
-	if err != nil {
66
-		return errors.Wrap(err, "error parsing plugin image reference")
67
-	}
68
-
69
-	// Make sure we can authenticate the request since the auth scope for plugin repos is different than a normal repo.
70
-	ctx = docker.WithScope(ctx, scope(ref, false))
71
-
72
-	// Make sure the fetch handler knows how to set a ref key for the plugin media type.
73
-	// Without this the ref key is "unknown" and we see a nasty warning message in the logs
74
-	ctx = remotes.WithMediaTypeKeyPrefix(ctx, mediaTypePluginConfig, "docker-plugin")
75
-
76
-	resolver, err := pm.newResolver(ctx, nil, auth, metaHeader, false)
77
-	if err != nil {
78
-		return err
79
-	}
80
-	resolved, desc, err := resolver.Resolve(ctx, withDomain.String())
81
-	if err != nil {
82
-		// This is backwards compatible with older versions of the distribution registry.
83
-		// The containerd client will add it's own accept header as a comma separated list of supported manifests.
84
-		// This is perfectly fine, unless you are talking to an older registry which does not split the comma separated list,
85
-		//   so it is never able to match a media type and it falls back to schema1 (yuck) and fails because our manifest the
86
-		//   fallback does not support plugin configs...
87
-		log.G(ctx).WithError(err).WithField("ref", withDomain).Debug("Error while resolving reference, falling back to backwards compatible accept header format")
88
-		headers := http.Header{}
89
-		headers.Add("Accept", c8dimages.MediaTypeDockerSchema2Manifest)
90
-		headers.Add("Accept", c8dimages.MediaTypeDockerSchema2ManifestList)
91
-		headers.Add("Accept", ocispec.MediaTypeImageManifest)
92
-		headers.Add("Accept", ocispec.MediaTypeImageIndex)
93
-		resolver, _ = pm.newResolver(ctx, nil, auth, headers, false)
94
-		if resolver != nil {
95
-			resolved, desc, err = resolver.Resolve(ctx, withDomain.String())
96
-			if err != nil {
97
-				log.G(ctx).WithError(err).WithField("ref", withDomain).Debug("Failed to resolve reference after falling back to backwards compatible accept header format")
98
-			}
99
-		}
100
-		if err != nil {
101
-			return errors.Wrap(err, "error resolving plugin reference")
102
-		}
103
-	}
104
-
105
-	fetcher, err := resolver.Fetcher(ctx, resolved)
106
-	if err != nil {
107
-		return errors.Wrap(err, "error creating plugin image fetcher")
108
-	}
109
-
110
-	fp := withFetchProgress(pm.blobStore, out, ref)
111
-	handlers = append([]c8dimages.Handler{fp, remotes.FetchHandler(pm.blobStore, fetcher)}, handlers...)
112
-	return c8dimages.Dispatch(ctx, c8dimages.Handlers(handlers...), nil, desc)
113
-}
114
-
115
-// applyLayer makes an c8dimages.HandlerFunc which applies a fetched image rootfs layer to a directory.
116
-//
117
-// TODO(@cpuguy83) This gets run sequentially after layer pull (makes sense), however
118
-// if there are multiple layers to fetch we may end up extracting layers in the wrong
119
-// order.
120
-func applyLayer(cs content.Store, dir string, out progress.Output) c8dimages.HandlerFunc {
121
-	return func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
122
-		switch desc.MediaType {
123
-		case
124
-			ocispec.MediaTypeImageLayer,
125
-			c8dimages.MediaTypeDockerSchema2Layer,
126
-			ocispec.MediaTypeImageLayerGzip,
127
-			c8dimages.MediaTypeDockerSchema2LayerGzip:
128
-		default:
129
-			return nil, nil
130
-		}
131
-
132
-		ra, err := cs.ReaderAt(ctx, desc)
133
-		if err != nil {
134
-			return nil, errors.Wrapf(err, "error getting content from content store for digest %s", desc.Digest)
135
-		}
136
-
137
-		id := stringid.TruncateID(desc.Digest.String())
138
-
139
-		rc := ioutils.NewReadCloserWrapper(content.NewReader(ra), ra.Close)
140
-		pr := progress.NewProgressReader(rc, out, desc.Size, id, "Extracting")
141
-		defer pr.Close()
142
-
143
-		if _, err := chrootarchive.ApplyLayer(dir, pr); err != nil {
144
-			return nil, errors.Wrapf(err, "error applying layer for digest %s", desc.Digest)
145
-		}
146
-		progress.Update(out, id, "Complete")
147
-		return nil, nil
148
-	}
149
-}
150
-
151
-func childrenHandler(cs content.Store) c8dimages.HandlerFunc {
152
-	ch := c8dimages.ChildrenHandler(cs)
153
-	return func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
154
-		switch desc.MediaType {
155
-		case mediaTypePluginConfig:
156
-			return nil, nil
157
-		default:
158
-			return ch(ctx, desc)
159
-		}
160
-	}
161
-}
162
-
163
-type fetchMeta struct {
164
-	blobs    []digest.Digest
165
-	config   digest.Digest
166
-	manifest digest.Digest
167
-}
168
-
169
-func storeFetchMetadata(m *fetchMeta) c8dimages.HandlerFunc {
170
-	return func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
171
-		switch desc.MediaType {
172
-		case
173
-			c8dimages.MediaTypeDockerSchema2LayerForeignGzip,
174
-			c8dimages.MediaTypeDockerSchema2Layer,
175
-			ocispec.MediaTypeImageLayer,
176
-			ocispec.MediaTypeImageLayerGzip:
177
-			m.blobs = append(m.blobs, desc.Digest)
178
-		case ocispec.MediaTypeImageManifest, c8dimages.MediaTypeDockerSchema2Manifest:
179
-			m.manifest = desc.Digest
180
-		case mediaTypePluginConfig:
181
-			m.config = desc.Digest
182
-		}
183
-		return nil, nil
184
-	}
185
-}
186
-
187
-func validateFetchedMetadata(md fetchMeta) error {
188
-	if md.config == "" {
189
-		return errors.New("fetched plugin image but plugin config is missing")
190
-	}
191
-	if md.manifest == "" {
192
-		return errors.New("fetched plugin image but manifest is missing")
193
-	}
194
-	return nil
195
-}
196
-
197
-// withFetchProgress is a fetch handler which registers a descriptor with a progress
198
-func withFetchProgress(cs content.Store, out progress.Output, ref reference.Named) c8dimages.HandlerFunc {
199
-	return func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
200
-		switch desc.MediaType {
201
-		case ocispec.MediaTypeImageManifest, c8dimages.MediaTypeDockerSchema2Manifest:
202
-			tn := reference.TagNameOnly(ref)
203
-			var tagOrDigest string
204
-			if tagged, ok := tn.(reference.Tagged); ok {
205
-				tagOrDigest = tagged.Tag()
206
-			} else {
207
-				tagOrDigest = tn.String()
208
-			}
209
-			progress.Messagef(out, tagOrDigest, "Pulling from %s", reference.FamiliarName(ref))
210
-			progress.Messagef(out, "", "Digest: %s", desc.Digest.String())
211
-			return nil, nil
212
-		case
213
-			c8dimages.MediaTypeDockerSchema2LayerGzip,
214
-			c8dimages.MediaTypeDockerSchema2Layer,
215
-			ocispec.MediaTypeImageLayer,
216
-			ocispec.MediaTypeImageLayerGzip:
217
-		default:
218
-			return nil, nil
219
-		}
220
-
221
-		id := stringid.TruncateID(desc.Digest.String())
222
-
223
-		if _, err := cs.Info(ctx, desc.Digest); err == nil {
224
-			out.WriteProgress(progress.Progress{ID: id, Action: "Already exists", LastUpdate: true})
225
-			return nil, nil
226
-		}
227
-
228
-		progress.Update(out, id, "Waiting")
229
-
230
-		key := remotes.MakeRefKey(ctx, desc)
231
-
232
-		go func() {
233
-			timer := time.NewTimer(100 * time.Millisecond)
234
-			if !timer.Stop() {
235
-				<-timer.C
236
-			}
237
-			defer timer.Stop()
238
-
239
-			var pulling bool
240
-			var (
241
-				// make sure we can still fetch from the content store
242
-				// if the main context is cancelled
243
-				// TODO: Might need to add some sort of timeout; see https://github.com/moby/moby/issues/49413
244
-				ctxErr      error
245
-				noCancelCTX = context.WithoutCancel(ctx)
246
-			)
247
-
248
-			for {
249
-				timer.Reset(100 * time.Millisecond)
250
-
251
-				select {
252
-				case <-ctx.Done():
253
-					ctxErr = ctx.Err()
254
-				case <-timer.C:
255
-				}
256
-
257
-				s, err := cs.Status(noCancelCTX, key)
258
-				if err != nil {
259
-					if !cerrdefs.IsNotFound(err) {
260
-						log.G(noCancelCTX).WithError(err).WithField("layerDigest", desc.Digest.String()).Error("Error looking up status of plugin layer pull")
261
-						progress.Update(out, id, err.Error())
262
-						return
263
-					}
264
-
265
-					if _, err := cs.Info(noCancelCTX, desc.Digest); err == nil {
266
-						progress.Update(out, id, "Download complete")
267
-						return
268
-					}
269
-
270
-					if ctxErr != nil {
271
-						progress.Update(out, id, ctxErr.Error())
272
-						return
273
-					}
274
-
275
-					continue
276
-				}
277
-
278
-				if !pulling {
279
-					progress.Update(out, id, "Pulling fs layer")
280
-					pulling = true
281
-				}
282
-
283
-				if s.Offset == s.Total {
284
-					out.WriteProgress(progress.Progress{ID: id, Action: "Download complete", Current: s.Offset, LastUpdate: true})
285
-					return
286
-				}
287
-
288
-				out.WriteProgress(progress.Progress{ID: id, Action: "Downloading", Current: s.Offset, Total: s.Total})
289
-			}
290
-		}()
291
-		return nil, nil
292
-	}
293
-}
294 1
deleted file mode 100644
... ...
@@ -1,370 +0,0 @@
1
-package plugin
2
-
3
-import (
4
-	"context"
5
-	"encoding/json"
6
-	"io"
7
-	"os"
8
-	"path/filepath"
9
-	"reflect"
10
-	"sort"
11
-	"strings"
12
-	"sync"
13
-	"syscall"
14
-
15
-	"github.com/containerd/containerd/v2/core/content"
16
-	"github.com/containerd/containerd/v2/plugins/content/local"
17
-	"github.com/containerd/log"
18
-	"github.com/docker/docker/api/types"
19
-	"github.com/docker/docker/api/types/events"
20
-	v2 "github.com/docker/docker/daemon/pkg/plugin/v2"
21
-	"github.com/docker/docker/internal/containerfs"
22
-	"github.com/docker/docker/internal/lazyregexp"
23
-	"github.com/docker/docker/pkg/authorization"
24
-	"github.com/docker/docker/registry"
25
-	"github.com/moby/pubsub"
26
-	"github.com/moby/sys/atomicwriter"
27
-	"github.com/opencontainers/go-digest"
28
-	"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 = lazyregexp.New(`^([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
-			// TODO(thaJeztah): make this fail if the plugin has "graphdriver" capability ?
208
-			if err := pm.restorePlugin(p, c); err != nil {
209
-				log.G(context.TODO()).WithError(err).WithField("id", p.GetID()).Error("Failed to restore plugin")
210
-				return
211
-			}
212
-
213
-			if p.Rootfs != "" {
214
-				p.Rootfs = filepath.Join(pm.config.Root, p.PluginObj.ID, "rootfs")
215
-			}
216
-
217
-			// We should only enable rootfs propagation for certain plugin types that need it.
218
-			for _, typ := range p.PluginObj.Config.Interface.Types {
219
-				if (typ.Capability == "volumedriver" || typ.Capability == "graphdriver" || typ.Capability == "csinode" || typ.Capability == "csicontroller") && typ.Prefix == "docker" && strings.HasPrefix(typ.Version, "1.") {
220
-					if p.PluginObj.Config.PropagatedMount != "" {
221
-						propRoot := filepath.Join(filepath.Dir(p.Rootfs), "propagated-mount")
222
-
223
-						if typ.Capability == "graphdriver" {
224
-							// TODO(thaJeztah): remove this for next release.
225
-							log.G(context.TODO()).WithError(err).WithField("dir", propRoot).Warn("skipping migrating propagated mount storage for deprecated graphdriver plugin")
226
-						}
227
-
228
-						// check if we need to migrate an older propagated mount from before
229
-						// these mounts were stored outside the plugin rootfs
230
-						if _, err := os.Stat(propRoot); os.IsNotExist(err) {
231
-							rootfsProp := filepath.Join(p.Rootfs, p.PluginObj.Config.PropagatedMount)
232
-							if _, err := os.Stat(rootfsProp); err == nil {
233
-								if err := os.Rename(rootfsProp, propRoot); err != nil {
234
-									log.G(context.TODO()).WithError(err).WithField("dir", propRoot).Error("error migrating propagated mount storage")
235
-								}
236
-							}
237
-						}
238
-
239
-						if err := os.MkdirAll(propRoot, 0o755); err != nil {
240
-							log.G(context.TODO()).Errorf("failed to create PropagatedMount directory at %s: %v", propRoot, err)
241
-						}
242
-					}
243
-				}
244
-			}
245
-
246
-			pm.save(p)
247
-			requiresManualRestore := !pm.config.LiveRestoreEnabled && p.IsEnabled()
248
-
249
-			if requiresManualRestore {
250
-				// if liveRestore is not enabled, the plugin will be stopped now so we should enable it
251
-				if err := pm.enable(p, c, true); err != nil {
252
-					log.G(context.TODO()).WithError(err).WithField("id", p.GetID()).Error("failed to enable plugin")
253
-				}
254
-			}
255
-		}(p)
256
-	}
257
-	wg.Wait()
258
-	return nil
259
-}
260
-
261
-// Get looks up the requested plugin in the store.
262
-func (pm *Manager) Get(idOrName string) (*v2.Plugin, error) {
263
-	return pm.config.Store.GetV2Plugin(idOrName)
264
-}
265
-
266
-func (pm *Manager) loadPlugin(id string) (*v2.Plugin, error) {
267
-	p := filepath.Join(pm.config.Root, id, configFileName)
268
-	dt, err := os.ReadFile(p)
269
-	if err != nil {
270
-		return nil, errors.Wrapf(err, "error reading %v", p)
271
-	}
272
-	var plugin v2.Plugin
273
-	if err := json.Unmarshal(dt, &plugin); err != nil {
274
-		return nil, errors.Wrapf(err, "error decoding %v", p)
275
-	}
276
-	return &plugin, nil
277
-}
278
-
279
-func (pm *Manager) save(p *v2.Plugin) error {
280
-	pluginJSON, err := json.Marshal(p)
281
-	if err != nil {
282
-		return errors.Wrap(err, "failed to marshal plugin json")
283
-	}
284
-	if err := atomicwriter.WriteFile(filepath.Join(pm.config.Root, p.GetID(), configFileName), pluginJSON, 0o600); err != nil {
285
-		return errors.Wrap(err, "failed to write atomically plugin json")
286
-	}
287
-	return nil
288
-}
289
-
290
-// GC cleans up unreferenced blobs. This is recommended to run in a goroutine
291
-func (pm *Manager) GC() {
292
-	pm.muGC.Lock()
293
-	defer pm.muGC.Unlock()
294
-
295
-	used := make(map[digest.Digest]struct{})
296
-	for _, p := range pm.config.Store.GetAll() {
297
-		used[p.Config] = struct{}{}
298
-		for _, b := range p.Blobsums {
299
-			used[b] = struct{}{}
300
-		}
301
-	}
302
-
303
-	ctx := context.TODO()
304
-	pm.blobStore.Walk(ctx, func(info content.Info) error {
305
-		_, ok := used[info.Digest]
306
-		if ok {
307
-			return nil
308
-		}
309
-
310
-		return pm.blobStore.Delete(ctx, info.Digest)
311
-	})
312
-}
313
-
314
-type logHook struct{ id string }
315
-
316
-func (logHook) Levels() []log.Level {
317
-	return []log.Level{
318
-		log.PanicLevel,
319
-		log.FatalLevel,
320
-		log.ErrorLevel,
321
-		log.WarnLevel,
322
-		log.InfoLevel,
323
-		log.DebugLevel,
324
-		log.TraceLevel,
325
-	}
326
-}
327
-
328
-func (l logHook) Fire(entry *log.Entry) error {
329
-	entry.Data = log.Fields{"plugin": l.id}
330
-	return nil
331
-}
332
-
333
-func makeLoggerStreams(id string) (stdout, stderr io.WriteCloser) {
334
-	logger := logrus.New()
335
-	logger.Hooks.Add(logHook{id})
336
-	return logger.WriterLevel(log.InfoLevel), logger.WriterLevel(log.ErrorLevel)
337
-}
338
-
339
-func validatePrivileges(requiredPrivileges, privileges types.PluginPrivileges) error {
340
-	if !isEqual(requiredPrivileges, privileges, isEqualPrivilege) {
341
-		return errors.New("incorrect privileges")
342
-	}
343
-
344
-	return nil
345
-}
346
-
347
-func isEqual(arrOne, arrOther types.PluginPrivileges, compare func(x, y types.PluginPrivilege) bool) bool {
348
-	if len(arrOne) != len(arrOther) {
349
-		return false
350
-	}
351
-
352
-	sort.Sort(arrOne)
353
-	sort.Sort(arrOther)
354
-
355
-	for i := 1; i < arrOne.Len(); i++ {
356
-		if !compare(arrOne[i], arrOther[i]) {
357
-			return false
358
-		}
359
-	}
360
-
361
-	return true
362
-}
363
-
364
-func isEqualPrivilege(a, b types.PluginPrivilege) bool {
365
-	if a.Name != b.Name {
366
-		return false
367
-	}
368
-
369
-	return reflect.DeepEqual(a.Value, b.Value)
370
-}
371 1
deleted file mode 100644
... ...
@@ -1,351 +0,0 @@
1
-package plugin
2
-
3
-import (
4
-	"context"
5
-	"encoding/json"
6
-	"net"
7
-	"os"
8
-	"path/filepath"
9
-	"time"
10
-
11
-	"github.com/containerd/containerd/v2/core/content"
12
-	"github.com/containerd/log"
13
-	"github.com/docker/docker/api/types"
14
-	"github.com/docker/docker/daemon/initlayer"
15
-	v2 "github.com/docker/docker/daemon/pkg/plugin/v2"
16
-	"github.com/docker/docker/errdefs"
17
-	"github.com/docker/docker/pkg/plugins"
18
-	"github.com/docker/docker/pkg/stringid"
19
-	"github.com/moby/sys/mount"
20
-	"github.com/opencontainers/go-digest"
21
-	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
22
-	"github.com/pkg/errors"
23
-	"golang.org/x/sys/unix"
24
-)
25
-
26
-func (pm *Manager) enable(p *v2.Plugin, c *controller, force bool) error {
27
-	p.Rootfs = filepath.Join(pm.config.Root, p.PluginObj.ID, "rootfs")
28
-	if p.IsEnabled() && !force {
29
-		return errors.Wrap(enabledError(p.Name()), "plugin already enabled")
30
-	}
31
-	spec, err := p.InitSpec(pm.config.ExecRoot)
32
-	if err != nil {
33
-		return err
34
-	}
35
-
36
-	c.restart = true
37
-	c.exitChan = make(chan bool)
38
-
39
-	pm.mu.Lock()
40
-	pm.cMap[p] = c
41
-	pm.mu.Unlock()
42
-
43
-	var propRoot string
44
-	if p.PluginObj.Config.PropagatedMount != "" {
45
-		propRoot = filepath.Join(filepath.Dir(p.Rootfs), "propagated-mount")
46
-
47
-		if err := os.MkdirAll(propRoot, 0o755); err != nil {
48
-			log.G(context.TODO()).Errorf("failed to create PropagatedMount directory at %s: %v", propRoot, err)
49
-		}
50
-
51
-		if err := mount.MakeRShared(propRoot); err != nil {
52
-			return errors.Wrap(err, "error setting up propagated mount dir")
53
-		}
54
-	}
55
-
56
-	rootFS := filepath.Join(pm.config.Root, p.PluginObj.ID, rootFSFileName)
57
-	if err := initlayer.Setup(rootFS, 0, 0); err != nil {
58
-		return errors.WithStack(err)
59
-	}
60
-
61
-	stdout, stderr := makeLoggerStreams(p.GetID())
62
-	if err := pm.executor.Create(p.GetID(), *spec, stdout, stderr); err != nil {
63
-		if p.PluginObj.Config.PropagatedMount != "" {
64
-			if err := mount.Unmount(propRoot); err != nil {
65
-				log.G(context.TODO()).WithField("plugin", p.Name()).WithError(err).Warn("Failed to unmount vplugin propagated mount root")
66
-			}
67
-		}
68
-		return errors.WithStack(err)
69
-	}
70
-	return pm.pluginPostStart(p, c)
71
-}
72
-
73
-func (pm *Manager) pluginPostStart(p *v2.Plugin, c *controller) error {
74
-	sockAddr := filepath.Join(pm.config.ExecRoot, p.GetID(), p.GetSocket())
75
-	p.SetTimeout(time.Duration(c.timeoutInSecs) * time.Second)
76
-	addr := &net.UnixAddr{Net: "unix", Name: sockAddr}
77
-	p.SetAddr(addr)
78
-
79
-	if p.Protocol() == plugins.ProtocolSchemeHTTPV1 {
80
-		client, err := plugins.NewClientWithTimeout(addr.Network()+"://"+addr.String(), nil, p.Timeout())
81
-		if err != nil {
82
-			c.restart = false
83
-			shutdownPlugin(p, c.exitChan, pm.executor)
84
-			return errors.WithStack(err)
85
-		}
86
-
87
-		p.SetPClient(client) //nolint:staticcheck // FIXME(thaJeztah): p.SetPClient is deprecated: Hardcoded plugin client is deprecated
88
-	}
89
-
90
-	// Initial sleep before net Dial to allow plugin to listen on socket.
91
-	time.Sleep(500 * time.Millisecond)
92
-	maxRetries := 3
93
-	var retries int
94
-	for {
95
-		// net dial into the unix socket to see if someone's listening.
96
-		conn, err := net.Dial("unix", sockAddr)
97
-		if err == nil {
98
-			conn.Close()
99
-			break
100
-		}
101
-
102
-		time.Sleep(3 * time.Second)
103
-		retries++
104
-
105
-		if retries > maxRetries {
106
-			log.G(context.TODO()).Debugf("error net dialing plugin: %v", err)
107
-			c.restart = false
108
-			// While restoring plugins, we need to explicitly set the state to disabled
109
-			pm.config.Store.SetState(p, false)
110
-			shutdownPlugin(p, c.exitChan, pm.executor)
111
-			return err
112
-		}
113
-	}
114
-	pm.config.Store.SetState(p, true)
115
-	pm.config.Store.CallHandler(p)
116
-
117
-	return pm.save(p)
118
-}
119
-
120
-func (pm *Manager) restore(p *v2.Plugin, c *controller) error {
121
-	stdout, stderr := makeLoggerStreams(p.GetID())
122
-	alive, err := pm.executor.Restore(p.GetID(), stdout, stderr)
123
-	if err != nil {
124
-		return err
125
-	}
126
-
127
-	if pm.config.LiveRestoreEnabled {
128
-		if !alive {
129
-			return pm.enable(p, c, true)
130
-		}
131
-
132
-		c.exitChan = make(chan bool)
133
-		c.restart = true
134
-		pm.mu.Lock()
135
-		pm.cMap[p] = c
136
-		pm.mu.Unlock()
137
-		return pm.pluginPostStart(p, c)
138
-	}
139
-
140
-	if alive {
141
-		// TODO(@cpuguy83): Should we always just re-attach to the running plugin instead of doing this?
142
-		c.restart = false
143
-		shutdownPlugin(p, c.exitChan, pm.executor)
144
-	}
145
-
146
-	return nil
147
-}
148
-
149
-const shutdownTimeout = 10 * time.Second
150
-
151
-func shutdownPlugin(p *v2.Plugin, ec chan bool, executor Executor) {
152
-	pluginID := p.GetID()
153
-
154
-	if err := executor.Signal(pluginID, unix.SIGTERM); err != nil {
155
-		log.G(context.TODO()).Errorf("Sending SIGTERM to plugin failed with error: %v", err)
156
-		return
157
-	}
158
-
159
-	timeout := time.NewTimer(shutdownTimeout)
160
-	defer timeout.Stop()
161
-
162
-	select {
163
-	case <-ec:
164
-		log.G(context.TODO()).Debug("Clean shutdown of plugin")
165
-	case <-timeout.C:
166
-		log.G(context.TODO()).Debug("Force shutdown plugin")
167
-		if err := executor.Signal(pluginID, unix.SIGKILL); err != nil {
168
-			log.G(context.TODO()).Errorf("Sending SIGKILL to plugin failed with error: %v", err)
169
-		}
170
-
171
-		timeout.Reset(shutdownTimeout)
172
-
173
-		select {
174
-		case <-ec:
175
-			log.G(context.TODO()).Debug("SIGKILL plugin shutdown")
176
-		case <-timeout.C:
177
-			log.G(context.TODO()).WithField("plugin", p.Name).Warn("Force shutdown plugin FAILED")
178
-		}
179
-	}
180
-}
181
-
182
-func (pm *Manager) disable(p *v2.Plugin, c *controller) error {
183
-	if !p.IsEnabled() {
184
-		return errors.Wrap(errDisabled(p.Name()), "plugin is already disabled")
185
-	}
186
-
187
-	c.restart = false
188
-	shutdownPlugin(p, c.exitChan, pm.executor)
189
-	pm.config.Store.SetState(p, false)
190
-	return pm.save(p)
191
-}
192
-
193
-// Shutdown stops all plugins and called during daemon shutdown.
194
-func (pm *Manager) Shutdown() {
195
-	plugins := pm.config.Store.GetAll()
196
-	for _, p := range plugins {
197
-		pm.mu.RLock()
198
-		c := pm.cMap[p]
199
-		pm.mu.RUnlock()
200
-
201
-		if pm.config.LiveRestoreEnabled && p.IsEnabled() {
202
-			log.G(context.TODO()).Debug("Plugin active when liveRestore is set, skipping shutdown")
203
-			continue
204
-		}
205
-		if pm.executor != nil && p.IsEnabled() {
206
-			c.restart = false
207
-			shutdownPlugin(p, c.exitChan, pm.executor)
208
-		}
209
-	}
210
-	if err := mount.RecursiveUnmount(pm.config.Root); err != nil {
211
-		log.G(context.TODO()).WithError(err).Warn("error cleaning up plugin mounts")
212
-	}
213
-}
214
-
215
-func (pm *Manager) upgradePlugin(p *v2.Plugin, configDigest, manifestDigest digest.Digest, blobsums []digest.Digest, tmpRootFSDir string, privileges *types.PluginPrivileges) (retErr error) {
216
-	config, err := pm.setupNewPlugin(configDigest, privileges)
217
-	if err != nil {
218
-		return err
219
-	}
220
-
221
-	pdir := filepath.Join(pm.config.Root, p.PluginObj.ID)
222
-	orig := filepath.Join(pdir, "rootfs")
223
-
224
-	// Make sure nothing is mounted
225
-	// This could happen if the plugin was disabled with `-f` with active mounts.
226
-	// If there is anything in `orig` is still mounted, this should error out.
227
-	if err := mount.RecursiveUnmount(orig); err != nil {
228
-		return errdefs.System(err)
229
-	}
230
-
231
-	backup := orig + "-old"
232
-	if err := os.Rename(orig, backup); err != nil {
233
-		return errors.Wrap(errdefs.System(err), "error backing up plugin data before upgrade")
234
-	}
235
-
236
-	defer func() {
237
-		if retErr != nil {
238
-			if err := os.RemoveAll(orig); err != nil {
239
-				log.G(context.TODO()).WithError(err).WithField("dir", backup).Error("error cleaning up after failed upgrade")
240
-				return
241
-			}
242
-			if err := os.Rename(backup, orig); err != nil {
243
-				retErr = errors.Wrap(err, "error restoring old plugin root on upgrade failure")
244
-			}
245
-			if err := os.RemoveAll(tmpRootFSDir); err != nil && !os.IsNotExist(err) {
246
-				log.G(context.TODO()).WithError(err).WithField("plugin", p.Name()).Errorf("error cleaning up plugin upgrade dir: %s", tmpRootFSDir)
247
-			}
248
-		} else {
249
-			if err := os.RemoveAll(backup); err != nil {
250
-				log.G(context.TODO()).WithError(err).WithField("dir", backup).Error("error cleaning up old plugin root after successful upgrade")
251
-			}
252
-
253
-			p.Config = configDigest
254
-			p.Blobsums = blobsums
255
-		}
256
-	}()
257
-
258
-	if err := os.Rename(tmpRootFSDir, orig); err != nil {
259
-		return errors.Wrap(errdefs.System(err), "error upgrading")
260
-	}
261
-
262
-	p.PluginObj.Config = config
263
-	p.Manifest = manifestDigest
264
-	if err := pm.save(p); err != nil {
265
-		return errors.Wrap(err, "error saving upgraded plugin config")
266
-	}
267
-
268
-	return nil
269
-}
270
-
271
-func (pm *Manager) setupNewPlugin(configDigest digest.Digest, privileges *types.PluginPrivileges) (types.PluginConfig, error) {
272
-	configRA, err := pm.blobStore.ReaderAt(context.TODO(), ocispec.Descriptor{Digest: configDigest})
273
-	if err != nil {
274
-		return types.PluginConfig{}, err
275
-	}
276
-	defer configRA.Close()
277
-
278
-	configR := content.NewReader(configRA)
279
-
280
-	var config types.PluginConfig
281
-	dec := json.NewDecoder(configR)
282
-	if err := dec.Decode(&config); err != nil {
283
-		return types.PluginConfig{}, errors.Wrapf(err, "failed to parse config")
284
-	}
285
-	if dec.More() {
286
-		return types.PluginConfig{}, errors.New("invalid config json")
287
-	}
288
-
289
-	requiredPrivileges := computePrivileges(config)
290
-	if privileges != nil {
291
-		if err := validatePrivileges(requiredPrivileges, *privileges); err != nil {
292
-			return types.PluginConfig{}, err
293
-		}
294
-	}
295
-
296
-	return config, nil
297
-}
298
-
299
-// createPlugin creates a new plugin. take lock before calling.
300
-func (pm *Manager) createPlugin(name string, configDigest, manifestDigest digest.Digest, blobsums []digest.Digest, rootFSDir string, privileges *types.PluginPrivileges, opts ...CreateOpt) (_ *v2.Plugin, retErr error) {
301
-	if err := pm.config.Store.validateName(name); err != nil { // todo: this check is wrong. remove store
302
-		return nil, errdefs.InvalidParameter(err)
303
-	}
304
-
305
-	config, err := pm.setupNewPlugin(configDigest, privileges)
306
-	if err != nil {
307
-		return nil, err
308
-	}
309
-
310
-	p := &v2.Plugin{
311
-		PluginObj: types.Plugin{
312
-			Name:   name,
313
-			ID:     stringid.GenerateRandomID(),
314
-			Config: config,
315
-		},
316
-		Config:   configDigest,
317
-		Blobsums: blobsums,
318
-		Manifest: manifestDigest,
319
-	}
320
-	p.InitEmptySettings()
321
-	for _, o := range opts {
322
-		o(p)
323
-	}
324
-
325
-	pdir := filepath.Join(pm.config.Root, p.PluginObj.ID)
326
-	if err := os.MkdirAll(pdir, 0o700); err != nil {
327
-		return nil, errors.Wrapf(err, "failed to mkdir %v", pdir)
328
-	}
329
-
330
-	defer func() {
331
-		if retErr != nil {
332
-			_ = os.RemoveAll(pdir)
333
-		}
334
-	}()
335
-
336
-	if err := os.Rename(rootFSDir, filepath.Join(pdir, rootFSFileName)); err != nil {
337
-		return nil, errors.Wrap(err, "failed to rename rootfs")
338
-	}
339
-
340
-	if err := pm.save(p); err != nil {
341
-		return nil, err
342
-	}
343
-
344
-	pm.config.Store.Add(p) // todo: remove
345
-
346
-	return p, nil
347
-}
348
-
349
-func recursiveUnmount(target string) error {
350
-	return mount.RecursiveUnmount(target)
351
-}
352 1
deleted file mode 100644
... ...
@@ -1,271 +0,0 @@
1
-package plugin
2
-
3
-import (
4
-	"io"
5
-	"net"
6
-	"os"
7
-	"path/filepath"
8
-	"syscall"
9
-	"testing"
10
-
11
-	"github.com/docker/docker/api/types"
12
-	"github.com/docker/docker/api/types/backend"
13
-	"github.com/docker/docker/api/types/events"
14
-	v2 "github.com/docker/docker/daemon/pkg/plugin/v2"
15
-	"github.com/docker/docker/internal/containerfs"
16
-	"github.com/docker/docker/pkg/stringid"
17
-	"github.com/moby/sys/mount"
18
-	"github.com/moby/sys/mountinfo"
19
-	"github.com/opencontainers/runtime-spec/specs-go"
20
-	"github.com/pkg/errors"
21
-	"gotest.tools/v3/skip"
22
-)
23
-
24
-func TestManagerWithPluginMounts(t *testing.T) {
25
-	skip.If(t, os.Getuid() != 0, "skipping test that requires root")
26
-
27
-	root := t.TempDir()
28
-	t.Cleanup(func() { _ = containerfs.EnsureRemoveAll(root) })
29
-
30
-	s := NewStore()
31
-	managerRoot := filepath.Join(root, "manager")
32
-	p1 := newTestPlugin(t, "test1", "testcap", managerRoot)
33
-
34
-	p2 := newTestPlugin(t, "test2", "testcap", managerRoot)
35
-	p2.PluginObj.Enabled = true
36
-
37
-	m, err := NewManager(
38
-		ManagerConfig{
39
-			Store:          s,
40
-			Root:           managerRoot,
41
-			ExecRoot:       filepath.Join(root, "exec"),
42
-			CreateExecutor: func(*Manager) (Executor, error) { return nil, nil },
43
-			LogPluginEvent: func(_, _ string, _ events.Action) {},
44
-		})
45
-	if err != nil {
46
-		t.Fatal(err)
47
-	}
48
-
49
-	if err := s.Add(p1); err != nil {
50
-		t.Fatal(err)
51
-	}
52
-	if err := s.Add(p2); err != nil {
53
-		t.Fatal(err)
54
-	}
55
-
56
-	// Create a mount to simulate a plugin that has created it's own mounts
57
-	p2Mount := filepath.Join(p2.Rootfs, "testmount")
58
-	if err := os.MkdirAll(p2Mount, 0o755); err != nil {
59
-		t.Fatal(err)
60
-	}
61
-	if err := mount.Mount("tmpfs", p2Mount, "tmpfs", ""); err != nil {
62
-		t.Fatal(err)
63
-	}
64
-
65
-	if err := m.Remove(p1.GetID(), &backend.PluginRmConfig{ForceRemove: true}); err != nil {
66
-		t.Fatal(err)
67
-	}
68
-	if mounted, err := mountinfo.Mounted(p2Mount); !mounted || err != nil {
69
-		t.Fatalf("expected %s to be mounted, err: %v", p2Mount, err)
70
-	}
71
-}
72
-
73
-func newTestPlugin(t *testing.T, name, capability, root string) *v2.Plugin {
74
-	id := stringid.GenerateRandomID()
75
-	rootfs := filepath.Join(root, id)
76
-	if err := os.MkdirAll(rootfs, 0o755); err != nil {
77
-		t.Fatal(err)
78
-	}
79
-
80
-	p := v2.Plugin{PluginObj: types.Plugin{ID: id, Name: name}}
81
-	p.Rootfs = rootfs
82
-	iType := types.PluginInterfaceType{Capability: capability, Prefix: "docker", Version: "1.0"}
83
-	i := types.PluginConfigInterface{Socket: "plugin.sock", Types: []types.PluginInterfaceType{iType}}
84
-	p.PluginObj.Config.Interface = i
85
-	p.PluginObj.ID = id
86
-
87
-	return &p
88
-}
89
-
90
-type simpleExecutor struct {
91
-	Executor
92
-}
93
-
94
-func (e *simpleExecutor) Create(id string, spec specs.Spec, stdout, stderr io.WriteCloser) error {
95
-	return errors.New("Create failed")
96
-}
97
-
98
-func TestCreateFailed(t *testing.T) {
99
-	root, err := os.MkdirTemp("", "test-create-failed")
100
-	if err != nil {
101
-		t.Fatal(err)
102
-	}
103
-	defer containerfs.EnsureRemoveAll(root)
104
-
105
-	s := NewStore()
106
-	managerRoot := filepath.Join(root, "manager")
107
-	p := newTestPlugin(t, "create", "testcreate", managerRoot)
108
-
109
-	m, err := NewManager(
110
-		ManagerConfig{
111
-			Store:          s,
112
-			Root:           managerRoot,
113
-			ExecRoot:       filepath.Join(root, "exec"),
114
-			CreateExecutor: func(*Manager) (Executor, error) { return &simpleExecutor{}, nil },
115
-			LogPluginEvent: func(_, _ string, _ events.Action) {},
116
-		})
117
-	if err != nil {
118
-		t.Fatal(err)
119
-	}
120
-
121
-	if err := s.Add(p); err != nil {
122
-		t.Fatal(err)
123
-	}
124
-
125
-	if err := m.enable(p, &controller{}, false); err == nil {
126
-		t.Fatalf("expected Create failed error, got %v", err)
127
-	}
128
-
129
-	if err := m.Remove(p.GetID(), &backend.PluginRmConfig{ForceRemove: true}); err != nil {
130
-		t.Fatal(err)
131
-	}
132
-}
133
-
134
-type executorWithRunning struct {
135
-	m         *Manager
136
-	root      string
137
-	exitChans map[string]chan struct{}
138
-}
139
-
140
-func (e *executorWithRunning) Create(id string, spec specs.Spec, stdout, stderr io.WriteCloser) error {
141
-	sockAddr := filepath.Join(e.root, id, "plugin.sock")
142
-	ch := make(chan struct{})
143
-	if e.exitChans == nil {
144
-		e.exitChans = make(map[string]chan struct{})
145
-	}
146
-	e.exitChans[id] = ch
147
-	listenTestPlugin(sockAddr, ch)
148
-	return nil
149
-}
150
-
151
-func (e *executorWithRunning) IsRunning(id string) (bool, error) {
152
-	return true, nil
153
-}
154
-
155
-func (e *executorWithRunning) Restore(id string, stdout, stderr io.WriteCloser) (bool, error) {
156
-	return true, nil
157
-}
158
-
159
-func (e *executorWithRunning) Signal(id string, signal syscall.Signal) error {
160
-	ch := e.exitChans[id]
161
-	ch <- struct{}{}
162
-	<-ch
163
-	e.m.HandleExitEvent(id)
164
-	return nil
165
-}
166
-
167
-func TestPluginAlreadyRunningOnStartup(t *testing.T) {
168
-	skip.If(t, os.Getuid() != 0, "skipping test that requires root")
169
-	t.Parallel()
170
-
171
-	root, err := os.MkdirTemp("", t.Name())
172
-	if err != nil {
173
-		t.Fatal(err)
174
-	}
175
-	defer containerfs.EnsureRemoveAll(root)
176
-
177
-	for _, test := range []struct {
178
-		desc   string
179
-		config ManagerConfig
180
-	}{
181
-		{
182
-			desc: "live-restore-disabled",
183
-			config: ManagerConfig{
184
-				LogPluginEvent: func(_, _ string, _ events.Action) {},
185
-			},
186
-		},
187
-		{
188
-			desc: "live-restore-enabled",
189
-			config: ManagerConfig{
190
-				LogPluginEvent:     func(_, _ string, _ events.Action) {},
191
-				LiveRestoreEnabled: true,
192
-			},
193
-		},
194
-	} {
195
-		t.Run(test.desc, func(t *testing.T) {
196
-			config := test.config
197
-			desc := test.desc
198
-			t.Parallel()
199
-
200
-			p := newTestPlugin(t, desc, desc, config.Root)
201
-			p.PluginObj.Enabled = true
202
-
203
-			// Need a short-ish path here so we don't run into unix socket path length issues.
204
-			config.ExecRoot, err = os.MkdirTemp("", "plugintest")
205
-
206
-			executor := &executorWithRunning{root: config.ExecRoot}
207
-			config.CreateExecutor = func(m *Manager) (Executor, error) { executor.m = m; return executor, nil }
208
-
209
-			if err := executor.Create(p.GetID(), specs.Spec{}, nil, nil); err != nil {
210
-				t.Fatal(err)
211
-			}
212
-
213
-			config.Root = filepath.Join(root, desc, "manager")
214
-			if err := os.MkdirAll(filepath.Join(config.Root, p.GetID()), 0o755); err != nil {
215
-				t.Fatal(err)
216
-			}
217
-
218
-			if !p.IsEnabled() {
219
-				t.Fatal("plugin should be enabled")
220
-			}
221
-			if err := (&Manager{config: config}).save(p); err != nil {
222
-				t.Fatal(err)
223
-			}
224
-
225
-			s := NewStore()
226
-			config.Store = s
227
-			if err != nil {
228
-				t.Fatal(err)
229
-			}
230
-			defer containerfs.EnsureRemoveAll(config.ExecRoot)
231
-
232
-			m, err := NewManager(config)
233
-			if err != nil {
234
-				t.Fatal(err)
235
-			}
236
-			defer m.Shutdown()
237
-
238
-			p = s.GetAll()[p.GetID()] // refresh `p` with what the manager knows
239
-
240
-			if p.Client() == nil { //nolint:staticcheck // FIXME(thaJeztah): p.Client is deprecated: use p.Addr() and manually create the client
241
-				t.Fatal("plugin client should not be nil")
242
-			}
243
-		})
244
-	}
245
-}
246
-
247
-func listenTestPlugin(sockAddr string, exit chan struct{}) (net.Listener, error) {
248
-	if err := os.MkdirAll(filepath.Dir(sockAddr), 0o755); err != nil {
249
-		return nil, err
250
-	}
251
-	l, err := net.Listen("unix", sockAddr)
252
-	if err != nil {
253
-		return nil, err
254
-	}
255
-	go func() {
256
-		for {
257
-			conn, err := l.Accept()
258
-			if err != nil {
259
-				return
260
-			}
261
-			conn.Close()
262
-		}
263
-	}()
264
-	go func() {
265
-		<-exit
266
-		l.Close()
267
-		os.Remove(sockAddr)
268
-		exit <- struct{}{}
269
-	}()
270
-	return l, nil
271
-}
272 1
deleted file mode 100644
... ...
@@ -1,55 +0,0 @@
1
-package plugin
2
-
3
-import (
4
-	"testing"
5
-
6
-	"github.com/docker/docker/api/types"
7
-)
8
-
9
-func TestValidatePrivileges(t *testing.T) {
10
-	testData := map[string]struct {
11
-		requiredPrivileges types.PluginPrivileges
12
-		privileges         types.PluginPrivileges
13
-		result             bool
14
-	}{
15
-		"diff-len": {
16
-			requiredPrivileges: []types.PluginPrivilege{
17
-				{Name: "Privilege1", Description: "Description", Value: []string{"abc", "def", "ghi"}},
18
-			},
19
-			privileges: []types.PluginPrivilege{
20
-				{Name: "Privilege1", Description: "Description", Value: []string{"abc", "def", "ghi"}},
21
-				{Name: "Privilege2", Description: "Description", Value: []string{"123", "456", "789"}},
22
-			},
23
-			result: false,
24
-		},
25
-		"diff-value": {
26
-			requiredPrivileges: []types.PluginPrivilege{
27
-				{Name: "Privilege1", Description: "Description", Value: []string{"abc", "def", "GHI"}},
28
-				{Name: "Privilege2", Description: "Description", Value: []string{"123", "456", "***"}},
29
-			},
30
-			privileges: []types.PluginPrivilege{
31
-				{Name: "Privilege1", Description: "Description", Value: []string{"abc", "def", "ghi"}},
32
-				{Name: "Privilege2", Description: "Description", Value: []string{"123", "456", "789"}},
33
-			},
34
-			result: false,
35
-		},
36
-		"diff-order-but-same-value": {
37
-			requiredPrivileges: []types.PluginPrivilege{
38
-				{Name: "Privilege1", Description: "Description", Value: []string{"abc", "def", "GHI"}},
39
-				{Name: "Privilege2", Description: "Description", Value: []string{"123", "456", "789"}},
40
-			},
41
-			privileges: []types.PluginPrivilege{
42
-				{Name: "Privilege2", Description: "Description", Value: []string{"123", "456", "789"}},
43
-				{Name: "Privilege1", Description: "Description", Value: []string{"GHI", "abc", "def"}},
44
-			},
45
-			result: true,
46
-		},
47
-	}
48
-
49
-	for key, data := range testData {
50
-		err := validatePrivileges(data.requiredPrivileges, data.privileges)
51
-		if (err == nil) != data.result {
52
-			t.Fatalf("Test item %s expected result to be %t, got %t", key, data.result, (err == nil))
53
-		}
54
-	}
55
-}
56 1
deleted file mode 100644
... ...
@@ -1,32 +0,0 @@
1
-package plugin
2
-
3
-import (
4
-	"fmt"
5
-
6
-	v2 "github.com/docker/docker/daemon/pkg/plugin/v2"
7
-	"github.com/opencontainers/runtime-spec/specs-go"
8
-)
9
-
10
-func (pm *Manager) enable(p *v2.Plugin, c *controller, force bool) error {
11
-	return fmt.Errorf("Not implemented")
12
-}
13
-
14
-func (pm *Manager) initSpec(p *v2.Plugin) (*specs.Spec, error) {
15
-	return nil, fmt.Errorf("Not implemented")
16
-}
17
-
18
-func (pm *Manager) disable(p *v2.Plugin, c *controller) error {
19
-	return fmt.Errorf("Not implemented")
20
-}
21
-
22
-func (pm *Manager) restore(p *v2.Plugin, c *controller) error {
23
-	return fmt.Errorf("Not implemented")
24
-}
25
-
26
-// Shutdown plugins
27
-func (pm *Manager) Shutdown() {
28
-}
29
-
30
-func recursiveUnmount(_ string) error {
31
-	return nil
32
-}
33 1
deleted file mode 100644
... ...
@@ -1,74 +0,0 @@
1
-package plugin
2
-
3
-import (
4
-	"sync"
5
-	"time"
6
-
7
-	"github.com/containerd/containerd/v2/core/remotes/docker"
8
-)
9
-
10
-func newPushJobs(tracker docker.StatusTracker) *pushJobs {
11
-	return &pushJobs{
12
-		names: make(map[string]string),
13
-		t:     tracker,
14
-	}
15
-}
16
-
17
-type pushJobs struct {
18
-	t docker.StatusTracker
19
-
20
-	mu   sync.Mutex
21
-	jobs []string
22
-	// maps job ref to a name
23
-	names map[string]string
24
-}
25
-
26
-func (p *pushJobs) add(id, name string) {
27
-	p.mu.Lock()
28
-	defer p.mu.Unlock()
29
-
30
-	if _, ok := p.names[id]; ok {
31
-		return
32
-	}
33
-	p.jobs = append(p.jobs, id)
34
-	p.names[id] = name
35
-}
36
-
37
-func (p *pushJobs) status() []contentStatus {
38
-	statuses := make([]contentStatus, 0, len(p.jobs))
39
-
40
-	p.mu.Lock()
41
-	defer p.mu.Unlock()
42
-
43
-	for _, j := range p.jobs {
44
-		var s contentStatus
45
-		s.Ref = p.names[j]
46
-
47
-		status, err := p.t.GetStatus(j)
48
-		if err != nil {
49
-			s.Status = "Waiting"
50
-		} else {
51
-			s.Total = status.Total
52
-			s.Offset = status.Offset
53
-			s.StartedAt = status.StartedAt
54
-			s.UpdatedAt = status.UpdatedAt
55
-			if status.UploadUUID == "" {
56
-				s.Status = "Upload complete"
57
-			} else {
58
-				s.Status = "Uploading"
59
-			}
60
-		}
61
-		statuses = append(statuses, s)
62
-	}
63
-
64
-	return statuses
65
-}
66
-
67
-type contentStatus struct {
68
-	Status    string
69
-	Total     int64
70
-	Offset    int64
71
-	StartedAt time.Time
72
-	UpdatedAt time.Time
73
-	Ref       string
74
-}
75 1
deleted file mode 100644
... ...
@@ -1,108 +0,0 @@
1
-package plugin
2
-
3
-import (
4
-	"context"
5
-	"crypto/tls"
6
-	"net"
7
-	"net/http"
8
-	"time"
9
-
10
-	"github.com/containerd/containerd/v2/core/remotes"
11
-	"github.com/containerd/containerd/v2/core/remotes/docker"
12
-	"github.com/containerd/log"
13
-	"github.com/distribution/reference"
14
-	"github.com/docker/docker/api/types/registry"
15
-	"github.com/docker/docker/dockerversion"
16
-	"github.com/pkg/errors"
17
-)
18
-
19
-// scope builds the correct auth scope for the registry client to authorize against
20
-// By default the client currently only does a "repository:" scope with out a classifier, e.g. "(plugin)"
21
-// Without this, the client will not be able to authorize the request
22
-func scope(ref reference.Named, push bool) string {
23
-	scope := "repository(plugin):" + reference.Path(reference.TrimNamed(ref)) + ":pull"
24
-	if push {
25
-		scope += ",push"
26
-	}
27
-	return scope
28
-}
29
-
30
-func (pm *Manager) newResolver(ctx context.Context, tracker docker.StatusTracker, auth *registry.AuthConfig, headers http.Header, httpFallback bool) (remotes.Resolver, error) {
31
-	if headers == nil {
32
-		headers = http.Header{}
33
-	}
34
-	headers.Add("User-Agent", dockerversion.DockerUserAgent(ctx))
35
-
36
-	return docker.NewResolver(docker.ResolverOptions{
37
-		Tracker: tracker,
38
-		Headers: headers,
39
-		Hosts:   pm.registryHostsFn(auth, httpFallback),
40
-	}), nil
41
-}
42
-
43
-func registryHTTPClient(config *tls.Config) *http.Client {
44
-	return &http.Client{
45
-		Transport: &http.Transport{
46
-			Proxy: http.ProxyFromEnvironment,
47
-			DialContext: (&net.Dialer{
48
-				Timeout:   30 * time.Second,
49
-				KeepAlive: 30 * time.Second,
50
-			}).DialContext,
51
-			TLSClientConfig:     config,
52
-			TLSHandshakeTimeout: 10 * time.Second,
53
-			IdleConnTimeout:     30 * time.Second,
54
-		},
55
-	}
56
-}
57
-
58
-func (pm *Manager) registryHostsFn(auth *registry.AuthConfig, httpFallback bool) docker.RegistryHosts {
59
-	return func(hostname string) ([]docker.RegistryHost, error) {
60
-		eps, err := pm.config.RegistryService.LookupPullEndpoints(hostname)
61
-		if err != nil {
62
-			return nil, errors.Wrapf(err, "error resolving repository for %s", hostname)
63
-		}
64
-
65
-		hosts := make([]docker.RegistryHost, 0, len(eps))
66
-
67
-		for _, ep := range eps {
68
-			// forced http fallback is used only for push since the containerd pusher only ever uses the first host we
69
-			// pass to it.
70
-			// So it is the callers responsibility to retry with this flag set.
71
-			if httpFallback && ep.URL.Scheme != "http" {
72
-				log.G(context.TODO()).WithField("registryHost", hostname).WithField("endpoint", ep).Debugf("Skipping non-http endpoint")
73
-				continue
74
-			}
75
-
76
-			caps := docker.HostCapabilityPull | docker.HostCapabilityResolve
77
-			if !ep.Mirror {
78
-				caps = caps | docker.HostCapabilityPush
79
-			}
80
-
81
-			host, err := docker.DefaultHost(ep.URL.Host)
82
-			if err != nil {
83
-				return nil, err
84
-			}
85
-
86
-			client := registryHTTPClient(ep.TLSConfig)
87
-			hosts = append(hosts, docker.RegistryHost{
88
-				Host:         host,
89
-				Scheme:       ep.URL.Scheme,
90
-				Client:       client,
91
-				Path:         "/v2",
92
-				Capabilities: caps,
93
-				Authorizer: docker.NewDockerAuthorizer(
94
-					docker.WithAuthClient(client),
95
-					docker.WithAuthCreds(func(_ string) (string, string, error) {
96
-						if auth.IdentityToken != "" {
97
-							return "", auth.IdentityToken, nil
98
-						}
99
-						return auth.Username, auth.Password, nil
100
-					}),
101
-				),
102
-			})
103
-		}
104
-		log.G(context.TODO()).WithField("registryHost", hostname).WithField("hosts", hosts).Debug("Resolved registry hosts")
105
-
106
-		return hosts, nil
107
-	}
108
-}
109 1
deleted file mode 100644
... ...
@@ -1,288 +0,0 @@
1
-package plugin
2
-
3
-import (
4
-	"context"
5
-	"fmt"
6
-	"strings"
7
-
8
-	"github.com/containerd/log"
9
-	"github.com/distribution/reference"
10
-	v2 "github.com/docker/docker/daemon/pkg/plugin/v2"
11
-	"github.com/docker/docker/errdefs"
12
-	"github.com/docker/docker/pkg/plugingetter"
13
-	"github.com/docker/docker/pkg/plugins"
14
-	"github.com/opencontainers/runtime-spec/specs-go"
15
-	"github.com/pkg/errors"
16
-)
17
-
18
-// allowV1PluginsFallback determines daemon's support for V1 plugins.
19
-// When the time comes to remove support for V1 plugins, flipping
20
-// this bool is all that will be needed.
21
-const allowV1PluginsFallback = true
22
-
23
-// defaultAPIVersion is the version of the plugin API for volume, network,
24
-// IPAM and authz. This is a very stable API. When we update this API, then
25
-// pluginType should include a version. e.g. "networkdriver/2.0".
26
-const defaultAPIVersion = "1.0"
27
-
28
-// GetV2Plugin retrieves a plugin by name, id or partial ID.
29
-func (ps *Store) GetV2Plugin(refOrID string) (*v2.Plugin, error) {
30
-	ps.RLock()
31
-	defer ps.RUnlock()
32
-
33
-	id, err := ps.resolvePluginID(refOrID)
34
-	if err != nil {
35
-		return nil, err
36
-	}
37
-
38
-	p, idOk := ps.plugins[id]
39
-	if !idOk {
40
-		return nil, errors.WithStack(errNotFound(id))
41
-	}
42
-
43
-	return p, nil
44
-}
45
-
46
-// validateName returns error if name is already reserved. always call with lock and full name
47
-func (ps *Store) validateName(name string) error {
48
-	for _, p := range ps.plugins {
49
-		if p.Name() == name {
50
-			return alreadyExistsError(name)
51
-		}
52
-	}
53
-	return nil
54
-}
55
-
56
-// GetAll retrieves all plugins.
57
-func (ps *Store) GetAll() map[string]*v2.Plugin {
58
-	ps.RLock()
59
-	defer ps.RUnlock()
60
-	return ps.plugins
61
-}
62
-
63
-// SetAll initialized plugins during daemon restore.
64
-func (ps *Store) SetAll(plugins map[string]*v2.Plugin) {
65
-	ps.Lock()
66
-	defer ps.Unlock()
67
-
68
-	for _, p := range plugins {
69
-		ps.setSpecOpts(p)
70
-	}
71
-	ps.plugins = plugins
72
-}
73
-
74
-func (ps *Store) getAllByCap(capability string) []plugingetter.CompatPlugin {
75
-	ps.RLock()
76
-	defer ps.RUnlock()
77
-
78
-	result := make([]plugingetter.CompatPlugin, 0, 1)
79
-	for _, p := range ps.plugins {
80
-		if p.IsEnabled() {
81
-			if _, err := p.FilterByCap(capability); err == nil {
82
-				result = append(result, p)
83
-			}
84
-		}
85
-	}
86
-	return result
87
-}
88
-
89
-// SetState sets the active state of the plugin and updates plugindb.
90
-func (ps *Store) SetState(p *v2.Plugin, state bool) {
91
-	ps.Lock()
92
-	defer ps.Unlock()
93
-
94
-	p.PluginObj.Enabled = state
95
-}
96
-
97
-func (ps *Store) setSpecOpts(p *v2.Plugin) {
98
-	var specOpts []SpecOpt
99
-	for _, typ := range p.GetTypes() {
100
-		opts, ok := ps.specOpts[typ.String()]
101
-		if ok {
102
-			specOpts = append(specOpts, opts...)
103
-		}
104
-	}
105
-
106
-	p.SetSpecOptModifier(func(s *specs.Spec) {
107
-		for _, o := range specOpts {
108
-			o(s)
109
-		}
110
-	})
111
-}
112
-
113
-// Add adds a plugin to memory and plugindb.
114
-// An error will be returned if there is a collision.
115
-func (ps *Store) Add(p *v2.Plugin) error {
116
-	ps.Lock()
117
-	defer ps.Unlock()
118
-
119
-	if v, exist := ps.plugins[p.GetID()]; exist {
120
-		return fmt.Errorf("plugin %q has the same ID %s as %q", p.Name(), p.GetID(), v.Name())
121
-	}
122
-
123
-	ps.setSpecOpts(p)
124
-
125
-	ps.plugins[p.GetID()] = p
126
-	return nil
127
-}
128
-
129
-// Remove removes a plugin from memory and plugindb.
130
-func (ps *Store) Remove(p *v2.Plugin) {
131
-	ps.Lock()
132
-	delete(ps.plugins, p.GetID())
133
-	ps.Unlock()
134
-}
135
-
136
-// Get returns an enabled plugin matching the given name and capability.
137
-func (ps *Store) Get(name, capability string, mode int) (plugingetter.CompatPlugin, error) {
138
-	// Lookup using new model.
139
-	if ps != nil {
140
-		p, err := ps.GetV2Plugin(name)
141
-		if err == nil {
142
-			if p.IsEnabled() {
143
-				fp, err := p.FilterByCap(capability)
144
-				if err != nil {
145
-					return nil, err
146
-				}
147
-				p.AddRefCount(mode)
148
-				return fp, nil
149
-			}
150
-
151
-			// Plugin was found but it is disabled, so we should not fall back to legacy plugins
152
-			// but we should error out right away
153
-			return nil, errDisabled(name)
154
-		}
155
-		var ierr errNotFound
156
-		if !errors.As(err, &ierr) {
157
-			return nil, err
158
-		}
159
-	}
160
-
161
-	if !allowV1PluginsFallback {
162
-		return nil, errNotFound(name)
163
-	}
164
-
165
-	p, err := plugins.Get(name, capability)
166
-	if err == nil {
167
-		return p, nil
168
-	}
169
-	if errors.Is(err, plugins.ErrNotFound) {
170
-		return nil, errNotFound(name)
171
-	}
172
-	return nil, errors.Wrap(errdefs.System(err), "legacy plugin")
173
-}
174
-
175
-// GetAllManagedPluginsByCap returns a list of managed plugins matching the given capability.
176
-func (ps *Store) GetAllManagedPluginsByCap(capability string) []plugingetter.CompatPlugin {
177
-	return ps.getAllByCap(capability)
178
-}
179
-
180
-// GetAllByCap returns a list of enabled plugins matching the given capability.
181
-func (ps *Store) GetAllByCap(capability string) ([]plugingetter.CompatPlugin, error) {
182
-	result := make([]plugingetter.CompatPlugin, 0, 1)
183
-
184
-	/* Daemon start always calls plugin.Init thereby initializing a store.
185
-	 * So store on experimental builds can never be nil, even while
186
-	 * handling legacy plugins. However, there are legacy plugin unit
187
-	 * tests where the volume subsystem directly talks with the plugin,
188
-	 * bypassing the daemon. For such tests, this check is necessary.
189
-	 */
190
-	if ps != nil {
191
-		result = ps.getAllByCap(capability)
192
-	}
193
-
194
-	// Lookup with legacy model
195
-	if allowV1PluginsFallback {
196
-		l := plugins.NewLocalRegistry()
197
-		pl, err := l.GetAll(capability)
198
-		if err != nil {
199
-			return nil, errors.Wrap(errdefs.System(err), "legacy plugin")
200
-		}
201
-		for _, p := range pl {
202
-			result = append(result, p)
203
-		}
204
-	}
205
-	return result, nil
206
-}
207
-
208
-func pluginType(capability string) string {
209
-	return fmt.Sprintf("docker.%s/%s", strings.ToLower(capability), defaultAPIVersion)
210
-}
211
-
212
-// Handle sets a callback for a given capability. It is only used by network
213
-// and ipam drivers during plugin registration. The callback registers the
214
-// driver with the subsystem (network, ipam).
215
-func (ps *Store) Handle(capability string, callback func(string, *plugins.Client)) {
216
-	typ := pluginType(capability)
217
-
218
-	// Register callback with new plugin model.
219
-	ps.Lock()
220
-	handlers, ok := ps.handlers[typ]
221
-	if !ok {
222
-		handlers = []func(string, *plugins.Client){}
223
-	}
224
-	handlers = append(handlers, callback)
225
-	ps.handlers[typ] = handlers
226
-	ps.Unlock()
227
-
228
-	// Register callback with legacy plugin model.
229
-	if allowV1PluginsFallback {
230
-		plugins.Handle(capability, callback)
231
-	}
232
-}
233
-
234
-// RegisterRuntimeOpt stores a list of SpecOpts for the provided capability.
235
-// These options are applied to the runtime spec before a plugin is started for the specified capability.
236
-func (ps *Store) RegisterRuntimeOpt(capability string, opts ...SpecOpt) {
237
-	ps.Lock()
238
-	defer ps.Unlock()
239
-	typ := pluginType(capability)
240
-	ps.specOpts[typ] = append(ps.specOpts[typ], opts...)
241
-}
242
-
243
-// CallHandler calls the registered callback. It is invoked during plugin enable.
244
-func (ps *Store) CallHandler(p *v2.Plugin) {
245
-	for _, typ := range p.GetTypes() {
246
-		for _, handler := range ps.handlers[typ.String()] {
247
-			handler(p.Name(), p.Client()) //nolint:staticcheck // FIXME(thaJeztah): p.Client is deprecated: use p.Addr() and manually create the client
248
-		}
249
-	}
250
-}
251
-
252
-// resolvePluginID must be protected by ps.RLock
253
-func (ps *Store) resolvePluginID(idOrName string) (string, error) {
254
-	if validFullID.MatchString(idOrName) {
255
-		return idOrName, nil
256
-	}
257
-
258
-	ref, err := reference.ParseNormalizedNamed(idOrName)
259
-	if err != nil {
260
-		return "", errors.WithStack(errNotFound(idOrName))
261
-	}
262
-	if _, ok := ref.(reference.Canonical); ok {
263
-		log.G(context.TODO()).Warnf("canonical references cannot be resolved: %v", reference.FamiliarString(ref))
264
-		return "", errors.WithStack(errNotFound(idOrName))
265
-	}
266
-
267
-	ref = reference.TagNameOnly(ref)
268
-
269
-	for _, p := range ps.plugins {
270
-		if p.PluginObj.Name == reference.FamiliarString(ref) {
271
-			return p.PluginObj.ID, nil
272
-		}
273
-	}
274
-
275
-	var found *v2.Plugin
276
-	for id, p := range ps.plugins { // this can be optimized
277
-		if strings.HasPrefix(id, idOrName) {
278
-			if found != nil {
279
-				return "", errors.WithStack(errAmbiguous(idOrName))
280
-			}
281
-			found = p
282
-		}
283
-	}
284
-	if found == nil {
285
-		return "", errors.WithStack(errNotFound(idOrName))
286
-	}
287
-	return found.PluginObj.ID, nil
288
-}
289 1
deleted file mode 100644
... ...
@@ -1,64 +0,0 @@
1
-package plugin
2
-
3
-import (
4
-	"testing"
5
-
6
-	"github.com/docker/docker/api/types"
7
-	v2 "github.com/docker/docker/daemon/pkg/plugin/v2"
8
-	"github.com/docker/docker/pkg/plugingetter"
9
-)
10
-
11
-func TestFilterByCapNeg(t *testing.T) {
12
-	p := v2.Plugin{PluginObj: types.Plugin{Name: "test:latest"}}
13
-	iType := types.PluginInterfaceType{Capability: "volumedriver", Prefix: "docker", Version: "1.0"}
14
-	i := types.PluginConfigInterface{Socket: "plugins.sock", Types: []types.PluginInterfaceType{iType}}
15
-	p.PluginObj.Config.Interface = i
16
-
17
-	_, err := p.FilterByCap("foobar")
18
-	if err == nil {
19
-		t.Fatalf("expected inadequate error, got %v", err)
20
-	}
21
-}
22
-
23
-func TestFilterByCapPos(t *testing.T) {
24
-	p := v2.Plugin{PluginObj: types.Plugin{Name: "test:latest"}}
25
-
26
-	iType := types.PluginInterfaceType{Capability: "volumedriver", Prefix: "docker", Version: "1.0"}
27
-	i := types.PluginConfigInterface{Socket: "plugins.sock", Types: []types.PluginInterfaceType{iType}}
28
-	p.PluginObj.Config.Interface = i
29
-
30
-	_, err := p.FilterByCap("volumedriver")
31
-	if err != nil {
32
-		t.Fatalf("expected no error, got %v", err)
33
-	}
34
-}
35
-
36
-func TestStoreGetPluginNotMatchCapRefs(t *testing.T) {
37
-	s := NewStore()
38
-	p := v2.Plugin{PluginObj: types.Plugin{Name: "test:latest"}}
39
-
40
-	iType := types.PluginInterfaceType{Capability: "whatever", Prefix: "docker", Version: "1.0"}
41
-	i := types.PluginConfigInterface{Socket: "plugins.sock", Types: []types.PluginInterfaceType{iType}}
42
-	p.PluginObj.Config.Interface = i
43
-
44
-	if err := s.Add(&p); err != nil {
45
-		t.Fatal(err)
46
-	}
47
-
48
-	if _, err := s.Get("test", "volumedriver", plugingetter.Acquire); err == nil {
49
-		t.Fatal("expected error when getting plugin that doesn't match the passed in capability")
50
-	}
51
-
52
-	if refs := p.GetRefCount(); refs != 0 {
53
-		t.Fatalf("reference count should be 0, got: %d", refs)
54
-	}
55
-
56
-	p.PluginObj.Enabled = true
57
-	if _, err := s.Get("test", "volumedriver", plugingetter.Acquire); err == nil {
58
-		t.Fatal("expected error when getting plugin that doesn't match the passed in capability")
59
-	}
60
-
61
-	if refs := p.GetRefCount(); refs != 0 {
62
-		t.Fatalf("reference count should be 0, got: %d", refs)
63
-	}
64
-}
... ...
@@ -12,7 +12,7 @@ import (
12 12
 	"github.com/docker/docker/api/types"
13 13
 	"github.com/docker/docker/api/types/events"
14 14
 	"github.com/docker/docker/api/types/registry"
15
-	"github.com/docker/docker/plugin"
15
+	"github.com/docker/docker/daemon/pkg/plugin"
16 16
 	registrypkg "github.com/docker/docker/registry"
17 17
 	"github.com/moby/go-archive"
18 18
 	"github.com/pkg/errors"